zerodha
¶
Zerodha historical candle download.
Kite serves candles at /instruments/historical/{token}/{interval}, three requests a second, and
goes back to 2005 for daily bars on long listed equities. oi=1 is always sent: for cash it
changes nothing, and for derivatives it adds open interest as a seventh element, so the response
is read by length rather than by fixed index.
Two behaviours are worth knowing. Kite caps how far one request may reach, and the cap differs by
interval - sixty days for minute bars, two thousand for daily - which is what makes a deep
backfill a walk rather than a single call. And an instrument that has left the current instrument
dump is refused with invalid token rather than an empty response, so an expired contract's
history is unreachable the moment it expires; the series is retired instead of retried.
Classes:
| Name | Description |
|---|---|
ZerodhaCandles |
Downloads Zerodha's historical candles. |
ZerodhaCandles
¶
Bases: BrokerCandles
Downloads Zerodha's historical candles.
Zerodha candle downloader.
apiis an authenticatedZerodhaAPI. One is constructed when not supplied, which establishes a session if the stored one has expired.
Example programs
"""Fetches one window of five minute RELIANCE futures bars from Kite and reads them into stored bars.
A `ZerodhaCandles` downloader is normally built with no arguments, and its constructor then opens a PostgreSQL connection for the progress table and logs in to Kite if the stored session has expired. Neither is wanted here, so the program builds the object without running its constructor, exactly as the offline suite `test_runs/candle_parse.py` does, and gives it a stand-in for the `ZerodhaAPI` object it would otherwise hold. `fetch_candles` and `parse_response` need nothing else.
The stand-in records the URL and query parameters it is asked for and answers with a response shaped like the one Kite returned on 2026-09-11, wrapped in the `status_code` and `data` envelope the real API class returns. Notice three things in the output: the stored interval name `5minute` is sent to Kite as `5minute` while `1minute` would have been sent as `minute`, `oi=1` is always asked for, and the seventh element of each candle becomes the bar's open interest.
Run it from the project root:
python examples/stock_brokers/instruments/historical/zerodha/ZerodhaCandles/example_1_fetching_five_minute_bars.py
"""
import datetime
from stock_brokers.instruments.historical.zerodha import (
ZerodhaCandles,
)
class RecordedKiteAPI:
"""A stand-in for `ZerodhaAPI` that answers every request with one recorded candle response.
Attributes:
requests (list): The (url, parameters) pairs it was asked for.
"""
def __init__(self):
"""Builds the stand-in with no requests recorded.
Returns:
None: This method returns nothing.
"""
self.requests = []
def get(self, url, params=None):
"""Records a GET request and answers it with recorded candles.
Args:
url (str): The URL the downloader asked for.
params (dict): The query parameters it sent.
Returns:
dict: The response envelope, with the candles under `data`.
"""
self.requests.append((url, params))
return {
'status_code': 200,
'data': {
'candles': [
[
'2026-09-11T09:15:00+0530',
1285.0,
1291.0,
1283.6,
1290.5,
225000,
128846500,
],
[
'2026-09-11T09:20:00+0530',
1290.5,
1292.2,
1288.1,
1289.0,
98500,
128901000,
],
],
},
}
class FetchingFiveMinuteBarsExample:
"""Asks for one window of five minute bars and prints the request and the bars.
Attributes:
api (RecordedKiteAPI): The stand-in Kite API.
candles (ZerodhaCandles): The downloader being shown.
"""
def __init__(self):
"""Builds the downloader around the stand-in API without opening a database connection.
Returns:
None: This method returns nothing.
"""
self.api = RecordedKiteAPI()
self.candles = object.__new__(ZerodhaCandles)
self.candles._api = self.api
def run(self):
"""Fetches the window, parses it and prints what was sent and what came back.
Returns:
None: This method returns nothing.
"""
payload = self.candles.fetch_candles(
'13368834',
'5minute',
datetime.date(2026, 9, 11),
datetime.date(2026, 9, 11),
)
url, parameters = self.api.requests[0]
print(f'Requested: {url}')
print(f'Parameters: {parameters}')
print(f'Candles in the payload: {len(payload["candles"])}')
bars = self.candles.parse_response(payload, '5minute')
for bar in bars:
bar_time, open_price, high, low, close, volume, open_interest = bar
print(f'{bar_time.isoformat()} open {open_price} high {high} low {low} close {close} volume {volume} oi {open_interest}')
print(f'Kite name for 1minute: {ZerodhaCandles.INTERVALS["1minute"]}')
if __name__ == '__main__':
FetchingFiveMinuteBarsExample().run()
It prints:
Requested: https://api.kite.trade/instruments/historical/13368834/5minute
Parameters: {'from': '2026-09-11', 'to': '2026-09-11', 'oi': 1}
Candles in the payload: 2
2026-09-11T09:15:00+05:30 open 1285.0 high 1291.0 low 1283.6 close 1290.5 volume 225000 oi 128846500
2026-09-11T09:20:00+05:30 open 1290.5 high 1292.2 low 1288.1 close 1289.0 volume 98500 oi 128901000
Kite name for 1minute: minute
"""Reads what a Kite instrument token says about its instrument, collapses a day bar onto its trading date, and shows an expired contract being refused.
Kite packs the exchange segment into the low byte of every instrument token and the exchange's own token into the bits above it, so RELIANCE on NSE is 738561, which is 2885 shifted left by eight bits with segment 1 in the low byte. `series_context` and `exchange_token` read that without any session, because they are a classmethod and a staticmethod. The program reads three tokens: NSE cash RELIANCE, the NIFTY 50 index (segment 9, which holds both exchanges' indices, so no exchange is named) and a token whose segment code Kite does not use.
It then parses a day bar stamped midnight India time, which `daily_bar_time` leaves on the same date, and skips a candle that is too short to read. Finally it fetches from a stand-in for `ZerodhaAPI` that raises the `ZerodhaAPIException` Kite's `invalid token` answer produces, which is what an instrument that has left the instrument dump gets. `fetch_candles` turns that into `CandleInstrumentUnknown`, the signal that retires the series rather than retrying it. The downloader is built without its constructor, as in `test_runs/candle_parse.py`, so no database connection is opened and no login happens.
Run it from the project root:
python examples/stock_brokers/instruments/historical/zerodha/ZerodhaCandles/example_2_tokens_day_bars_and_refusals.py
"""
import datetime
from stock_brokers.api.zerodha import (
ZerodhaAPIException,
)
from stock_brokers.instruments.historical.base import (
CandleInstrumentUnknown,
)
from stock_brokers.instruments.historical.zerodha import (
ZerodhaCandles,
)
class RefusingKiteAPI:
"""A stand-in for `ZerodhaAPI` that refuses every request the way Kite refuses an expired contract."""
def get(self, url, params=None):
"""Refuses a GET request with Kite's invalid token error.
Args:
url (str): The URL the downloader asked for.
params (dict): The query parameters it sent.
Returns:
dict: Never returns.
Raises:
ZerodhaAPIException: Always, with Kite's `InputException` code.
"""
raise ZerodhaAPIException(code='InputException', message='invalid token')
class TokensDayBarsAndRefusalsExample:
"""Reads tokens, parses a day bar and handles a refused window.
Attributes:
candles (ZerodhaCandles): The downloader being shown.
"""
def __init__(self):
"""Builds the downloader around the refusing stand-in without opening a database connection.
Returns:
None: This method returns nothing.
"""
self.candles = object.__new__(ZerodhaCandles)
self.candles._api = RefusingKiteAPI()
def show_tokens(self):
"""Prints what each of three tokens says about its instrument.
Returns:
None: This method returns nothing.
"""
tokens = [
'738561',
'256265',
'738560',
]
for token in tokens:
context = ZerodhaCandles.series_context(token)
if context.segments:
segments = f'{len(context.segments)} segments starting {context.segments[0]}'
else:
segments = 'any segment'
print(f'{token}: exchange {context.exchange}, {segments}, exchange token {context.exchange_token}')
print(f'Exchange token inside 738561: {ZerodhaCandles.exchange_token("738561")}')
def show_day_bars(self):
"""Parses a day bar and a candle too short to read.
Returns:
None: This method returns nothing.
"""
payload = {
'candles': [
[
'2026-09-11T00:00:00+0530',
1267.0,
1267.4,
1253.0,
1257.5,
8777736,
],
[
'2026-09-12T00:00:00+0530',
1257.5,
],
],
}
bars = self.candles.parse_response(payload, 'day')
print(f'Day bars read: {len(bars)} of {len(payload["candles"])}')
print(f'Day bar time: {bars[0][0].isoformat()}, close {bars[0][4]}, open interest {bars[0][6]}')
def show_refusal(self):
"""Fetches a window of an expired contract and prints how the refusal is classified.
Returns:
None: This method returns nothing.
"""
try:
self.candles.fetch_candles(
'13368834',
'day',
datetime.date(2026, 1, 1),
datetime.date(2026, 9, 11),
)
except CandleInstrumentUnknown as error:
print(f'Refused as {type(error).__name__}: {error}')
def run(self):
"""Runs the three parts in turn.
Returns:
None: This method returns nothing.
"""
self.show_tokens()
self.show_day_bars()
self.show_refusal()
if __name__ == '__main__':
TokensDayBarsAndRefusalsExample().run()
It prints:
738561: exchange nse, 6 segments starting nse_equities, exchange token 2885
256265: exchange None, 8 segments starting nse_equity_indices, exchange token None
738560: exchange None, any segment, exchange token None
Exchange token inside 738561: 2885
Day bars read: 1 of 2
Day bar time: 2026-09-11T00:00:00+05:30, close 1257.5, open interest None
Refused as CandleInstrumentUnknown: ('InputException', 'invalid token')
Methods:
| Name | Description |
|---|---|
series_context |
Read the exchange and segment out of a Kite instrument token. |
exchange_token |
The exchange's own token inside a Kite instrument token. |
fetch_candles |
Fetch one window of Kite candles. |
parse_response |
Turn Kite's candle list into bars. |
instruments |
Every instrument this broker has ever published, with the fields priority needs. |
priority_for |
Where a series sits in the queue. Lower is worked first. |
seed |
Make sure every instrument and interval this broker serves has a progress row. |
claim |
The next series due for a window, or None when there is nothing to do. |
next_window |
The date range to ask for next, as a |
relogin |
Establish a fresh session and rebuild everything built on the old one. |
after_relogin |
Re-establish anything a downloader keeps on top of the broker session. Nothing by default. |
visit |
Fetch one window for one series and store it, advancing the watermark. |
run |
Work the queue until stopped, out of work, or out of time. |
summary |
How far the download has got, for a status display. |
close |
Close the database connection. |
Source code in stock_brokers/instruments/historical/zerodha.py
series_context
classmethod
¶
Read the exchange and segment out of a Kite instrument token.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
identifier
|
str
|
A Kite instrument token, for example "738561". |
required |
Returns:
| Type | Description |
|---|---|
SeriesContext
|
The token itself, with the exchange and segments its low byte names. |
Example usage
Source code in stock_brokers/instruments/historical/zerodha.py
exchange_token
staticmethod
¶
The exchange's own token inside a Kite instrument token.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
identifier
|
str
|
A Kite instrument token. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The exchange token, for example "2885" for 738561. |
Example usage
print(f'Exchange token inside 738561: {ZerodhaCandles.exchange_token("738561")}')
Source code in stock_brokers/instruments/historical/zerodha.py
fetch_candles
¶
Fetch one window of Kite candles.
tokenis the Kite instrument token.intervalis the stored interval name.start_dateandend_datebound the window, inclusive.
Example usage
payload = self.candles.fetch_candles(
'13368834',
'5minute',
datetime.date(2026, 9, 11),
datetime.date(2026, 9, 11),
)
self.candles.fetch_candles(
'13368834',
'day',
datetime.date(2026, 1, 1),
datetime.date(2026, 9, 11),
)
Source code in stock_brokers/instruments/historical/zerodha.py
_classify
staticmethod
¶
Decide what a Kite error means for the series being downloaded.
The distinction that matters is between "this instrument will never work", which retires the series, and everything else, which is retried with backoff. Kite reports the first as an InputException naming an invalid token.
exceptionis what the API call raised.
Source code in stock_brokers/instruments/historical/zerodha.py
parse_response
¶
Turn Kite's candle list into bars.
Kite returns [timestamp, open, high, low, close, volume], and a seventh element for
open interest when oi=1 applies. Read by length: a cash instrument simply has six.
payloadis thedataobject from the response.intervalis the stored interval name, which decides whether the bar time is left alone or collapsed onto its trading date.
Example usage
Source code in stock_brokers/instruments/historical/zerodha.py
instruments
¶
Every instrument this broker has ever published, with the fields priority needs.
The most recent snapshot each token appeared in, not merely today's: a contract that expired last month is still worth asking about, and the answer decides whether it is retired. Today's snapshot alone would silently narrow the universe to whatever is live.
Example usage
Source code in stock_brokers/instruments/historical/base.py
priority_for
¶
Where a series sits in the queue. Lower is worked first.
The ordering is the difference between useful data in days and useful data in months. Cash instruments come first because they have the deepest history and are wanted most; expired options come last because there are tens of thousands of them, each holding a few weeks of data, and most will be refused outright. Coarse intervals beat fine ones within each tier, since a day bar covers a year in one request and a minute bar covers two months.
instrument_typeis the broker's own type string.expiryis the contract expiry, or None for cash.intervalis the stored interval name.
Example usage
priority = self.downloader.priority_for(instrument_type, expiry, interval)
Source code in stock_brokers/instruments/historical/base.py
_interval_minutes
staticmethod
¶
Minutes in an interval name like 15minute, or 0 when it is not a minute interval.
intervalis the stored interval name.
Source code in stock_brokers/instruments/historical/base.py
seed
¶
Make sure every instrument and interval this broker serves has a progress row.
Safe to run again: existing rows are left exactly as they are, so re-seeding after a new instrument master picks up what is new without disturbing what is already downloaded. Returns how many rows were added.
Example usage
print(f'Series added by the first seed: {self.downloader.seed()}')
print(f'Series added by a second seed: {self.downloader.seed()}')
Source code in stock_brokers/instruments/historical/base.py
_insert_series
¶
Register one batch of series, leaving any that already exist exactly as they are.
cursoris an open cursor on this broker's connection.batchis a list of(token, interval, priority, seeded_date)tuples.
Source code in stock_brokers/instruments/historical/base.py
claim
¶
The next series due for a window, or None when there is nothing to do.
Ordered by priority and then by how long a series has been waiting. Series that have reached the broker's limit, or that are backing off after failures, are skipped.
Example usage
Source code in stock_brokers/instruments/historical/base.py
next_window
¶
The date range to ask for next, as a (start, end, direction) triple.
The backward walk comes first and runs to completion. A series starts at today and steps back a window at a time until it reaches the broker's earliest date or the broker stops returning anything, and only then does it start filling forward with whatever has happened since.
Doing it the other way round - preferring forward whenever the last stored bar is older than today - looks reasonable and does not work: outside market hours the last bar is always older than today, so the series asks for the same empty recent window forever and never walks back at all.
seriesis a row fromclaim.
Example usage
start_date, end_date, direction = self.downloader.next_window(series)
Source code in stock_brokers/instruments/historical/base.py
_fetch_bars
¶
Fetch and parse one window, logging in once first if the broker refuses the session.
A token expiring does not mean the download is over, and it does not mean this series is at
fault. The rule is to log in first and then carry on: one login, then the same window
again. Only if that login fails, or the fresh session is refused as well, does the error go
back up - where visit and run stop the broker rather than carry on with a dead token.
Both alternatives are worse. Stopping outright leaves the download waiting for systemd to restart it into a login, and recording the refusal against whichever series it was on and moving to the next sends thousands of refused requests an hour - enough for Cloudflare to ban the machine from Fyers.
The login is re-armed by any window that comes back without a session error, so a backfill that runs for weeks gets its one login at each daily expiry.
token,interval,start_dateandend_dateare as forfetch_candles.
Source code in stock_brokers/instruments/historical/base.py
relogin
¶
Establish a fresh session and rebuild everything built on the old one.
Through ensure_session, so it takes the same Redis lock and obeys the same floor on login
attempts as any other process using it - which means it may wait, and may find another
process has already logged in, in which case that session is simply used.
Raises:
| Type | Description |
|---|---|
CandleAuthenticationError
|
If no working session could be established. |
Example usage
Source code in stock_brokers/instruments/historical/base.py
_build_api
¶
Construct this broker's authenticated API class, around whatever session is now stored.
The broker's own class from api_class_for, which is what every downloader builds. A
downloader that builds its API differently overrides this.
Source code in stock_brokers/instruments/historical/base.py
after_relogin
¶
Re-establish anything a downloader keeps on top of the broker session. Nothing by default.
For a broker whose historical endpoint takes a second credential minted from the session, such as a separate market data token, this is where that credential is renewed.
Example usage
Source code in stock_brokers/instruments/historical/base.py
visit
¶
Fetch one window for one series and store it, advancing the watermark.
Returns the number of bars stored. The bars and the watermark are written in the same transaction, so an interruption costs at most the window in flight - and repeating that window is harmless, because bars upsert on their key.
seriesis a row fromclaim.
Example usage
stored = downloader.visit(self.series('738561'))
Source code in stock_brokers/instruments/historical/base.py
_store
¶
Write a window's bars and move the watermark, in one transaction.
tokenis the broker's instrument identifier.intervalis the stored interval name.barsare the parsed(time, open, high, low, close, volume, oi)tuples.requested_fromis the start of the window that was asked for.directionis whether the walk was forward or backward.previous_latestis the newest bar already stored, used to tell a forward window that gained ground from one that returned only what was already known.
Source code in stock_brokers/instruments/historical/base.py
_value_within_bigint
¶
Returns a bar's count, or None when the count cannot fit in a BIGINT column.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token
|
str
|
The broker's instrument identifier. |
required |
interval
|
str
|
The stored interval name. |
required |
bar_time
|
datetime
|
The time of the bar the value belongs to. |
required |
column
|
str
|
The name of the column the value is written to, either "volume" or "oi". |
required |
value
|
int | float | None
|
The count exactly as the broker sent it. |
required |
Returns:
| Type | Description |
|---|---|
int | float | None
|
The value unchanged when it fits in a BIGINT or is not a number, and None when it does not fit. |
Source code in stock_brokers/instruments/historical/base.py
_record_empty
¶
Note that a window came back with nothing, and step past it.
token,intervalidentify the series.streakis how many empty windows in a row this makes.requested_fromis the start of the window that was asked for.directionis whether the walk was forward or backward.
Source code in stock_brokers/instruments/historical/base.py
_finish_backfill
¶
Mark the backward walk complete, leaving the series collecting forward.
Distinct from _retire: the broker has no more past for this series, but it will keep
producing new bars. Retiring it here would quietly stop a live instrument updating.
token,intervalidentify the series.reasonis why the walk stopped, kept for review.
Source code in stock_brokers/instruments/historical/base.py
_defer_until_tomorrow
¶
Put a series aside until the next day.
Reached when a forward window brings nothing new, which outside market hours is every forward window. Without it the highest priority series would be re-fetched continuously and spend the whole rate limit re-reading bars already stored.
token,intervalidentify the series.reasonis recorded as the outcome.
Source code in stock_brokers/instruments/historical/base.py
_record_throttle
¶
Note that the broker asked us to slow down, without holding it against the series.
The window is left exactly as it was, so the same series is claimed again once the limiter's back off has passed and the work is simply redone.
token,intervalidentify the series.reasonis what the broker said.
Source code in stock_brokers/instruments/historical/base.py
_record_failure
¶
Note a failure and push the next attempt out exponentially.
An hour, then two, four, eight and so on, so a series that is permanently broken stops crowding out the ones that are not.
token,intervalidentify the series.reasonis what went wrong, stored for a human to read.
Source code in stock_brokers/instruments/historical/base.py
_retire
¶
Stop asking for a series the broker will not serve.
token,intervalidentify the series.reasonis why, stored so the decision can be reviewed.
Source code in stock_brokers/instruments/historical/base.py
run
¶
Work the queue until stopped, out of work, or out of time.
Returns a (visits, bars) pair.
stop_eventis athreading.Eventthat ends the loop when set.deadline_secondsends the loop after this long, for a job with a nightly window.
Example usage
print(f'A run asked to stop before it starts: {self.downloader.run(stop_event=stop_event)}')
Source code in stock_brokers/instruments/historical/base.py
summary
¶
How far the download has got, for a status display.
Example usage
Source code in stock_brokers/instruments/historical/base.py
close
¶
Close the database connection.
Example usage