shoonya
¶
Shoonya historical candle download.
Shoonya runs the Noren platform, so the two endpoints, the refusal shapes and the field names all
live in noren.py, as it does for Flattrade. What is
particular to Shoonya is here: its host, its rate, and how far back its deployment keeps data.
Measured on 2026-09-12 against RELIANCE: EODChartData holds a rolling five years, reaching
2021-09-13 and no further, and TPSeries a rolling year of intraday bars with January to April
2026 missing from the middle of it - the same hole Flattrade has, which suggests it belongs to
the platform rather than to either broker.
Shoonya also lists NCDEX, which Flattrade does not. That costs nothing here: the exchange comes straight out of Shoonya's own scrip files rather than being translated, so an exchange the other deployment has never heard of needs no code at all.
Classes:
| Name | Description |
|---|---|
ShoonyaCandles |
Downloads Shoonya's historical candles. |
ShoonyaCandles
¶
Bases: NorenCandles
Downloads Shoonya's historical candles.
Example programs
"""Fetches a week of RELIANCE daily bars from Shoonya's `EODChartData` endpoint and reads them.
Shoonya runs the Noren platform, so `ShoonyaCandles` adds only what is particular to it to `NorenCandles`: its host, its rate of one request a second (a tenth of Flattrade's) and a daily history that is a rolling five years. The program prints those settings and fetches one daily window, which goes to Shoonya's host addressed by `NSE:RELIANCE-EQ` with the window as epoch seconds at midnight India time.
A `ShoonyaCandles` constructor opens a PostgreSQL connection and logs in to Shoonya when the stored session has expired, so the program builds the object without running its constructor, as `test_runs/candle_parse.py` does, and gives it a stand-in for `ShoonyaAPI`. The stand-in records the POST and answers with two daily bars, each a JSON encoded string as Noren sends them, wrapped in the `status_code` and `data` envelope the real API class returns. Notice that the bars, sent newest first, come back oldest first, and that every bar lands on midnight India time on its trading date and has no open interest.
Run it from the project root:
python examples/stock_brokers/instruments/historical/shoonya/ShoonyaCandles/example_1_daily_bars_from_shoonya.py
"""
import datetime
import json
from stock_brokers.instruments.historical.shoonya import (
ShoonyaCandles,
)
class RecordedShoonyaAPI:
"""A stand-in for `ShoonyaAPI` that answers `EODChartData` with two recorded daily bars.
Attributes:
requests (list): The (url, body) pairs it was sent.
"""
def __init__(self):
"""Builds the stand-in with no requests recorded.
Returns:
None: This method returns nothing.
"""
self.requests = []
def post(self, url, data=None):
"""Records a POST request and answers it with two daily bars.
Args:
url (str): The endpoint the downloader called.
data (dict): The request fields it sent.
Returns:
dict: The response envelope, with the bars under `data`.
"""
self.requests.append((url, data))
thursday = {
'time': '10-SEP-2026',
'into': '1271.20',
'inth': '1276.00',
'intl': '1264.10',
'intc': '1267.00',
'ssboe': '1788998400',
'intv': '6120455.00',
}
friday = {
'time': '11-SEP-2026',
'into': '1267.00',
'inth': '1267.40',
'intl': '1253.00',
'intc': '1257.50',
'ssboe': '1789084800',
'intv': '8777736.00',
}
return {
'status_code': 200,
'data': [
json.dumps(friday),
json.dumps(thursday),
],
}
class DailyBarsFromShoonyaExample:
"""Prints Shoonya's settings, then fetches and parses one daily window.
Attributes:
api (RecordedShoonyaAPI): The stand-in Shoonya API.
candles (ShoonyaCandles): 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 = RecordedShoonyaAPI()
self.candles = object.__new__(ShoonyaCandles)
self.candles._api = self.api
def run(self):
"""Prints the settings, fetches the week to 2026-09-11 and prints the bars.
Returns:
None: This method returns nothing.
"""
print(f'Broker: {ShoonyaCandles.BROKER_NAME}')
print(f'Host: {ShoonyaCandles.BASE_URL}')
print(f'Requests a second: {ShoonyaCandles.REQUESTS_PER_SECOND}')
print(f'Earliest date: {ShoonyaCandles.EARLIEST_AVAILABLE_DATE}')
payload = self.candles.fetch_candles(
'NSE|2885|RELIANCE-EQ',
'day',
datetime.date(2026, 9, 7),
datetime.date(2026, 9, 11),
)
url, body = self.api.requests[0]
print(f'Requested {url}')
print(f' body: {body}')
for bar in self.candles.parse_response(payload, 'day'):
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}')
if __name__ == '__main__':
DailyBarsFromShoonyaExample().run()
It prints:
Broker: shoonya
Host: https://api.shoonya.com/NorenWClientAPI
Requests a second: 1.0
Earliest date: 2021-01-01
Requested https://api.shoonya.com/NorenWClientAPI/EODChartData
body: {'sym': 'NSE:RELIANCE-EQ', 'from': '1788719400', 'to': '1789151400'}
2026-09-10T00:00:00+05:30 open 1271.2 high 1276.0 low 1264.1 close 1267.0 volume 6120455 oi None
2026-09-11T00:00:00+05:30 open 1267.0 high 1267.4 low 1253.0 close 1257.5 volume 8777736 oi None
"""Reads a Shoonya NCDEX series identifier, fetches its minute bars, and shows Shoonya throttling a request.
Shoonya lists NCDEX, which Flattrade does not, and needs no code of its own for it: the exchange name `NCX` comes straight from Shoonya's scrip files, and `series_context` reads it as the `ncdex` exchange with derivative segments. The window is then asked of `TPSeries` with that exchange name.
Shoonya allows about one request a second, and a Noren throttle arrives as HTTP 200 carrying an object with `stat` of `Not_Ok` and a `Rate_Limited` message. `parse_response` raises that as `CandleThrottled`, which makes the base class back off and retry the same window rather than count it against the series. The stand-in for `ShoonyaAPI` records the POST and answers with that throttle, wrapped in the `status_code` and `data` envelope the real API class returns.
A `ShoonyaCandles` constructor opens a PostgreSQL connection and logs in to Shoonya when the stored session has expired, so the program builds the object without running its constructor, as `test_runs/candle_parse.py` does.
Run it from the project root:
python examples/stock_brokers/instruments/historical/shoonya/ShoonyaCandles/example_2_ncdex_series_and_throttle.py
"""
import datetime
from stock_brokers.instruments.historical.base import (
CandleThrottled,
)
from stock_brokers.instruments.historical.shoonya import (
ShoonyaCandles,
)
class ThrottlingShoonyaAPI:
"""A stand-in for `ShoonyaAPI` that answers every request with Noren's rate limit refusal.
Attributes:
requests (list): The (url, body) pairs it was sent.
"""
def __init__(self):
"""Builds the stand-in with no requests recorded.
Returns:
None: This method returns nothing.
"""
self.requests = []
def post(self, url, data=None):
"""Records a POST request and refuses it for rate.
Args:
url (str): The endpoint the downloader called.
data (dict): The request fields it sent.
Returns:
dict: The response envelope, with the refusal object under `data`.
"""
self.requests.append((url, data))
return {
'status_code': 200,
'data': {
'stat': 'Not_Ok',
'emsg': 'Rate_Limited: too many requests',
},
}
class NcdexSeriesAndThrottleExample:
"""Reads an NCDEX identifier and fetches a window the stand-in throttles.
Attributes:
api (ThrottlingShoonyaAPI): The stand-in Shoonya API.
candles (ShoonyaCandles): 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 = ThrottlingShoonyaAPI()
self.candles = object.__new__(ShoonyaCandles)
self.candles._api = self.api
def run(self):
"""Reads the identifier, fetches one window and prints the throttle.
Returns:
None: This method returns nothing.
"""
identifier = 'NCX|61452|GUARSEED10DEC26'
context = ShoonyaCandles.series_context(identifier)
print(f'{identifier}: token {context.broker_token}, exchange {context.exchange}, {len(context.segments)} derivative segments to search')
day = datetime.date(2026, 9, 11)
payload = self.candles.fetch_candles(identifier, '5minute', day, day)
url, body = self.api.requests[0]
print(f'Requested {url}')
print(f' body: {body}')
try:
self.candles.parse_response(payload, '5minute')
except CandleThrottled as error:
print(f'{type(error).__name__}: {error}')
if __name__ == '__main__':
NcdexSeriesAndThrottleExample().run()
It prints:
NCX|61452|GUARSEED10DEC26: token 61452, exchange ncdex, 16 derivative segments to search
Requested https://api.shoonya.com/NorenWClientAPI/TPSeries
body: {'exch': 'NCX', 'token': '61452', 'st': '1789065000', 'et': '1789151400', 'intrv': '5'}
CandleThrottled: shoonya refused the request for rate: Rate_Limited: too many requests
Noren candle downloader.
apiis an authenticated API class for this broker. One is built when not supplied, which establishes a session if the stored one has expired.
Example programs
"""Shows a Noren platform downloader asking its two endpoints, `TPSeries` for intraday bars and `EODChartData` for daily ones, and reading both answers.
`NorenCandles` holds everything Flattrade and Shoonya share, and a real broker supplies only its name, host, rate, earliest date and the API class it logs in with. The program writes such a subclass for a made-up deployment at `noren.example.com`, so what is shown is the platform's behaviour rather than either broker's. Its constructor would open a PostgreSQL connection and log in, so the object is built without running it, as `test_runs/candle_parse.py` does, and the stand-in API is the one its `_build_api` returns.
The stored identifier `NSE|2885|RELIANCE-EQ` carries all three things the two endpoints need: `TPSeries` takes the exchange and numeric token with the window as epoch seconds, and `EODChartData` takes `NSE:RELIANCE-EQ`. The stand-in API records each POST and answers with bars shaped like the ones both hosts returned on 2026-09-11. Notice that the intraday answer arrives newest first and is sorted oldest first, that each bar's volume is its own `intv` rather than the day's running total `v` (188567 and 7938419 rather than 8776602), and that the daily bar arrives as a JSON encoded string and lands on midnight India time.
Run it from the project root:
python examples/stock_brokers/instruments/historical/noren/NorenCandles/example_1_intraday_and_daily_endpoints.py
"""
import datetime
import json
from stock_brokers.instruments.historical.noren import (
NorenCandles,
)
class RecordedNorenAPI:
"""A stand-in for a Noren broker's API class that answers each endpoint with recorded bars.
Attributes:
requests (list): The (url, body) pairs it was sent.
"""
def __init__(self):
"""Builds the stand-in with no requests recorded.
Returns:
None: This method returns nothing.
"""
self.requests = []
def post(self, url, data=None):
"""Records a POST request and answers it with recorded bars.
Args:
url (str): The endpoint the downloader called.
data (dict): The request fields it sent.
Returns:
dict: The response envelope, with the decoded body under `data`.
"""
self.requests.append((url, data))
if url.endswith('/TPSeries'):
body = [
{
'stat': 'Ok',
'time': '11-09-2026 15:29:00',
'ssboe': '1789120740',
'into': '1257.50',
'inth': '1257.50',
'intl': '1257.50',
'intc': '1257.50',
'intv': '7938419',
'v': '8776602',
'oi': '0',
},
{
'stat': 'Ok',
'time': '11-09-2026 09:15:00',
'ssboe': '1789098300',
'into': '1266.40',
'inth': '1267.40',
'intl': '1261.50',
'intc': '1262.70',
'intv': '188567',
'v': '188567',
'oi': '0',
},
]
else:
bar = {
'time': '11-SEP-2026',
'into': '1267.00',
'inth': '1267.40',
'intl': '1253.00',
'intc': '1257.50',
'ssboe': '1789084800',
'intv': '8777736.00',
}
body = [
json.dumps(bar),
]
return {
'status_code': 200,
'data': body,
}
class ExampleDeploymentCandles(NorenCandles):
"""Downloads candles from a made-up Noren deployment, to show what a broker subclass supplies."""
BROKER_NAME = 'example_noren'
BASE_URL = 'https://noren.example.com/NorenWClientAPI'
REQUESTS_PER_SECOND = 2.0
EARLIEST_AVAILABLE_DATE = datetime.date(2020, 1, 1)
def _build_api(self):
"""Builds the stand-in API in place of a logged-in broker API class.
Returns:
RecordedNorenAPI: A new stand-in.
"""
return RecordedNorenAPI()
class IntradayAndDailyEndpointsExample:
"""Fetches and parses one intraday and one daily window.
Attributes:
api (RecordedNorenAPI): The stand-in API the downloader was given.
candles (ExampleDeploymentCandles): The downloader being shown.
"""
def __init__(self):
"""Builds the downloader without opening a database connection and gives it its API.
Returns:
None: This method returns nothing.
"""
self.candles = object.__new__(ExampleDeploymentCandles)
self.api = self.candles._build_api()
self.candles._api = self.api
def fetch_and_print(self, interval):
"""Fetches 2026-09-11 for RELIANCE at one interval and prints the request and the bars.
Args:
interval (str): The stored interval name.
Returns:
None: This method returns nothing.
"""
day = datetime.date(2026, 9, 11)
payload = self.candles.fetch_candles('NSE|2885|RELIANCE-EQ', interval, day, day)
url, body = self.api.requests[-1]
print(f'{interval}: {url}')
print(f' body: {body}')
for bar in self.candles.parse_response(payload, interval):
bar_time, open_price, high, low, close, volume, open_interest = bar
india_time = bar_time.astimezone(datetime.timezone(datetime.timedelta(hours=5, minutes=30)))
print(f' {india_time.isoformat()} open {open_price} high {high} low {low} close {close} volume {volume} oi {open_interest}')
def run(self):
"""Fetches the intraday window and then the daily one.
Returns:
None: This method returns nothing.
"""
self.fetch_and_print('1minute')
self.fetch_and_print('day')
if __name__ == '__main__':
IntradayAndDailyEndpointsExample().run()
It prints:
1minute: https://noren.example.com/NorenWClientAPI/TPSeries
body: {'exch': 'NSE', 'token': '2885', 'st': '1789065000', 'et': '1789151400', 'intrv': '1'}
2026-09-11T09:15:00+05:30 open 1266.4 high 1267.4 low 1261.5 close 1262.7 volume 188567 oi 0
2026-09-11T15:29:00+05:30 open 1257.5 high 1257.5 low 1257.5 close 1257.5 volume 7938419 oi 0
day: https://noren.example.com/NorenWClientAPI/EODChartData
body: {'sym': 'NSE:RELIANCE-EQ', 'from': '1789065000', 'to': '1789151400'}
2026-09-11T00:00:00+05:30 open 1267.0 high 1267.4 low 1253.0 close 1257.5 volume 8777736 oi None
"""Builds Noren identifiers from an instrument master, reads what each says about its instrument, and shows how a Noren refusal object is read.
A Noren token is unique only within its exchange's scrip file, and the two endpoints need different fields, so `instruments` joins the exchange, the token and the trading symbol into one identifier and turns Noren's `27-OCT-2026` expiry into a date. `identifier_parts` splits it again, and `series_context`, a classmethod that needs no downloader, reads the exchange and segments. For NSE and BSE it also gives the exchange symbol with the series suffix removed, so `RELIANCE-EQ` becomes `RELIANCE`; an exchange name Noren's table does not know leaves everything but the token open.
A Noren refusal is a JSON object with `stat` and `emsg`, sent with HTTP 200. Its "no data" message is the same for an empty window and an unknown instrument, so `parse_response` reads it as no bars; only a throttle and an expired session raise, as `CandleThrottled` and `CandleAuthenticationError`, and the message names the broker.
The program uses `ShoonyaCandles`, the Noren subclass for Shoonya, so the instance has a broker name to report; everything shown is defined on `NorenCandles`. The downloader is built without its constructor, as in `test_runs/candle_parse.py`, and given a stand-in database connection whose cursor answers the instrument master query with four rows, so nothing connects to PostgreSQL or logs in.
Run it from the project root:
python examples/stock_brokers/instruments/historical/noren/NorenCandles/example_2_identifiers_and_refusal_objects.py
"""
from stock_brokers.instruments.historical.base import (
CandleError,
)
from stock_brokers.instruments.historical.noren import (
NorenCandles,
)
from stock_brokers.instruments.historical.shoonya import (
ShoonyaCandles,
)
class ScripFileCursor:
"""A stand-in database cursor that answers the instrument master query with fixed rows.
Attributes:
rows (list): The rows `fetchall` returns.
"""
def __init__(self, rows):
"""Holds the rows to answer with.
Args:
rows (list): Tuples of (exchange, token, trading symbol, instrument, expiry).
Returns:
None: This method returns nothing.
"""
self.rows = rows
def __enter__(self):
"""Enters the `with` block.
Returns:
ScripFileCursor: This cursor.
"""
return self
def __exit__(self, exception_type, exception, traceback):
"""Leaves the `with` block without suppressing an exception.
Args:
exception_type (type | None): The exception's class, if one was raised.
exception (BaseException | None): The exception, if one was raised.
traceback (types.TracebackType | None): Its traceback, if one was raised.
Returns:
bool: Always False.
"""
return False
def execute(self, query):
"""Accepts the query without running it.
Args:
query (str): The SQL the downloader sent.
Returns:
None: This method returns nothing.
"""
def fetchall(self):
"""Returns the fixed rows.
Returns:
list: The rows.
"""
return self.rows
class ScripFileConnection:
"""A stand-in PostgreSQL connection over four rows of Noren scrip files.
Attributes:
rows (list): The rows every cursor answers with.
"""
def __init__(self):
"""Builds the connection with four scrip file rows.
Returns:
None: This method returns nothing.
"""
self.rows = [
(
'NSE',
'2885',
'RELIANCE-EQ',
'EQ',
None,
),
(
'NFO',
'2885',
'RELIANCE27OCT26F',
'FUTSTK',
'27-OCT-2026',
),
(
'MCX',
'565899',
'CRUDEOIL19OCT26',
'FUTCOM',
'19-OCT-2026',
),
(
'NSE',
'26000',
'Nifty 50',
'INDEX',
'',
),
]
def cursor(self):
"""Opens a cursor.
Returns:
ScripFileCursor: A cursor over the rows.
"""
return ScripFileCursor(self.rows)
def commit(self):
"""Accepts a commit.
Returns:
None: This method returns nothing.
"""
class IdentifiersAndRefusalObjectsExample:
"""Lists identifiers, reads them, and parses three refusal objects.
Attributes:
candles (ShoonyaCandles): The downloader being shown.
"""
def __init__(self):
"""Builds the downloader around the stand-in connection without running its constructor.
Returns:
None: This method returns nothing.
"""
self.candles = object.__new__(ShoonyaCandles)
self.candles._connection = ScripFileConnection()
def show_identifiers(self):
"""Lists the instruments and reads each identifier, then one with an unknown exchange.
Returns:
None: This method returns nothing.
"""
identifiers = []
for identifier, instrument_type, expiry in self.candles.instruments():
print(f'{identifier}: {instrument_type}, expiry {expiry}')
identifiers.append(identifier)
identifiers.append('XYZ|123|UNKNOWN')
for identifier in identifiers:
exchange_name, token, trading_symbol = self.candles.identifier_parts(identifier)
context = NorenCandles.series_context(identifier)
if context.segments:
segments = f'{len(context.segments)} segments'
else:
segments = 'any segment'
print(f' {exchange_name} {token} {trading_symbol}: exchange {context.exchange}, {segments}, symbol {context.symbol}')
def show_refusals(self):
"""Parses three refusal objects and prints what each became.
Returns:
None: This method returns nothing.
"""
messages = [
'Error Occurred : 5 "no data"',
'Rate_Limited: too many requests',
'Session Expired : Invalid Session Key',
]
for message in messages:
refusal = {
'stat': 'Not_Ok',
'emsg': message,
}
try:
bars = self.candles.parse_response(refusal, '1minute')
outcome = f'{len(bars)} bars'
except CandleError as error:
outcome = f'{type(error).__name__}: {error}'
print(f'"{message}": {outcome}')
def run(self):
"""Runs both parts in turn.
Returns:
None: This method returns nothing.
"""
self.show_identifiers()
self.show_refusals()
if __name__ == '__main__':
IdentifiersAndRefusalObjectsExample().run()
It prints:
NSE|2885|RELIANCE-EQ: EQ, expiry None
NFO|2885|RELIANCE27OCT26F: FUTSTK, expiry 2026-10-27
MCX|565899|CRUDEOIL19OCT26: FUTCOM, expiry 2026-10-19
NSE|26000|Nifty 50: INDEX, expiry None
NSE 2885 RELIANCE-EQ: exchange nse, 10 segments, symbol RELIANCE
NFO 2885 RELIANCE27OCT26F: exchange nse, 16 segments, symbol None
MCX 565899 CRUDEOIL19OCT26: exchange mcx, 16 segments, symbol None
NSE 26000 Nifty 50: exchange nse, 10 segments, symbol Nifty 50
XYZ 123 UNKNOWN: exchange None, any segment, symbol None
"Error Occurred : 5 "no data"": 0 bars
"Rate_Limited: too many requests": CandleThrottled: shoonya refused the request for rate: Rate_Limited: too many requests
"Session Expired : Invalid Session Key": CandleAuthenticationError: shoonya rejected the session key: Session Expired : Invalid Session Key
Methods:
| Name | Description |
|---|---|
series_context |
Read the exchange and segment out of a Noren identifier. |
fetch_candles |
Fetch one window of Noren candles. |
parse_response |
Turn a Noren response into bars. |
instruments |
Every instrument this deployment has published, identified for both endpoints. |
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. |
identifier_parts |
Split a stored Noren identifier into the fields the two endpoints need. |
Source code in stock_brokers/instruments/historical/noren.py
_build_api
¶
series_context
classmethod
¶
Read the exchange and segment out of a Noren identifier.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
identifier
|
str
|
For example "NSE|2885|RELIANCE-EQ". |
required |
Returns:
| Type | Description |
|---|---|
SeriesContext
|
The numeric token, with the exchange and segments its exchange names. |
Example usage
Source code in stock_brokers/instruments/historical/noren.py
fetch_candles
¶
Fetch one window of Noren candles.
The API class wraps the body into the jData=...&jKey=... form Noren expects and adds the
user id, so only the fields particular to the request are named here.
tokenis theEXCHANGE|token|tradingsymbolidentifier frominstruments.intervalis the stored interval name.start_dateandend_datebound the window, inclusive.
Example usage
payload = self.candles.fetch_candles('NSE|2885|RELIANCE-EQ', interval, day, day)
Source code in stock_brokers/instruments/historical/noren.py
parse_response
¶
Turn a Noren response into bars.
A refusal object is answered with no bars rather than raised, for the reason in
_classify: its message cannot tell an empty window from a dead instrument.
EODChartData returns each bar as a JSON encoded string inside the list, so a string
element is decoded before it is read. The volume taken is intv, the bar's own volume;
the v beside it is the day's running total, and taking that by mistake would produce a
rising series that looks plausible on a chart and is wrong everywhere.
payloadis the decoded response, a list of bars or a refusal object.intervalis the stored interval name.
Example usage
bars = self.candles.parse_response(refusal, '1minute')
Source code in stock_brokers/instruments/historical/noren.py
instruments
¶
Every instrument this deployment has published, identified for both endpoints.
The two endpoints want different things - TPSeries an exchange and a numeric token,
EODChartData an exchange joined to a trading symbol - so all three are stored as one
identifier and split again when the request is built. One progress row then serves both.
A Noren token is unique only within its scrip file: the same number appears under NSE and under NFO, some sixteen hundred times on this snapshot, which is why the exchange is part of the identifier rather than a detail recovered later.
The expiry is converted from Noren's 27-OCT-2026 to a date, because priority_for reads
dates and would otherwise queue every expired contract as though it were live.
Example usage
Source code in stock_brokers/instruments/historical/noren.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
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
_expiry_date
staticmethod
¶
Turn Noren's 27-OCT-2026 expiry into a date, or None when there is none.
expiryis theexpirycolumn.
Source code in stock_brokers/instruments/historical/noren.py
_epoch_seconds
staticmethod
¶
Midnight India time on a date, as epoch seconds, which is what Noren's windows take.
dayis the date to convert.
Source code in stock_brokers/instruments/historical/noren.py
identifier_parts
staticmethod
¶
Split a stored Noren identifier into the fields the two endpoints need.
The identifier is built in instruments and taken apart here and nowhere else, so the two
cannot drift apart.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
identifier
|
str
|
For example "NSE|2885|RELIANCE-EQ". |
required |
Returns:
| Type | Description |
|---|---|
tuple
|
The exchange, the numeric token and the trading symbol. |
Example usage
exchange_name, token, trading_symbol = self.candles.identifier_parts(identifier)
Source code in stock_brokers/instruments/historical/noren.py
_classify
¶
Decide what a Noren refusal means for the series being downloaded.
Nothing here retires a series. Noren reports an unknown instrument and an empty window with the same message, so the only safe reading of that message is "no bars", and a dead instrument is retired by the base class when its walk ends having collected none.
exceptionis what the API call raised.
Source code in stock_brokers/instruments/historical/noren.py
_raise_for_refusal
¶
Raise for the refusals that mean something other than an empty window.
Noren answers HTTP 200 with an object when it refuses, so a throttle and an expired session arrive here rather than as exceptions from the API class.
bodyis the refusal object, carryingstatandemsg.