fyers
¶
Fyers historical candle download.
Fyers is addressed by ticker rather than by token: the request carries NSE:RELIANCE-EQ, not the
fytoken the instrument master is keyed on, so the ticker is what the progress table stores.
Four things were settled against the live API on 2026-09-12 rather than taken from the documentation, which is a JavaScript application rather than a page.
The endpoint answers at api-t1.fyers.in/data/history, and its authorization header is the
application id joined to the access token by a colon with no Bearer prefix - which is what this
project's FyersAPI already sends.
The range caps are stated in the refusal: one hundred days for every intraday resolution, three
hundred and sixty six for 1D, 1W and 1M. They are refusals, not truncations.
Daily history reaches 2000: RELIANCE answers for every year from 2000 and no_data for 1995.
An empty window comes back as HTTP 200 with s holding no_data, so emptiness has to be read
from that field rather than from the status line. A candle row is six elements for cash and seven
for a derivative, the seventh being open interest, so it is read by length.
Fyers stamps a daily bar at midnight UTC where Kite stamps midnight India time - the same trading
day, five and a half hours apart - which is why day, week and month bars go through
daily_bar_time before they are stored.
Classes:
| Name | Description |
|---|---|
FyersCandles |
Downloads Fyers' historical candles. |
FyersCandles
¶
Bases: BrokerCandles
Downloads Fyers' historical candles.
Fyers candle downloader.
apiis an authenticatedFyersAPI. One is constructed when not supplied, which establishes a session if the stored one has expired.
Example programs
"""Fetches two windows of RELIANCE day bars from Fyers, one with bars and one Fyers reports as `no_data`.
Fyers is addressed by ticker, so the request carries `NSE:RELIANCE-EQ` together with a resolution code (`1D` for the stored `day`), and it always asks for open interest and continuous data. Fyers stamps a day bar at midnight UTC where Kite stamps midnight India time, which is the same trading day five and a half hours apart. `parse_response` moves every day bar onto midnight India time, so the bar stamped 2026-08-03 00:00 UTC is stored as 2026-08-03 00:00 India time. An empty window comes back as HTTP 200 with `s` holding `no_data`, and is read as no bars rather than an error.
A `FyersCandles` constructor opens a PostgreSQL connection and logs in to Fyers 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 `FyersAPI`. The stand-in records each request and answers with a body shaped like Fyers' recorded answers, wrapped in the `status_code` and `data` envelope the real API class returns.
Run it from the project root:
python examples/stock_brokers/instruments/historical/fyers/FyersCandles/example_1_day_bars_stamped_in_utc.py
"""
import datetime
from stock_brokers.instruments.historical.fyers import (
FyersCandles,
)
class RecordedFyersAPI:
"""A stand-in for `FyersAPI` that answers a window in 2026 with two day bars and any older window with `no_data`.
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.
Args:
url (str): The URL the downloader asked for.
params (dict): The query parameters it sent.
Returns:
dict: The response envelope, with Fyers' body under `data`.
"""
self.requests.append((url, params))
if params['range_from'] < '2000-01-01':
body = {
's': 'no_data',
'candles': [],
}
else:
body = {
's': 'ok',
'candles': [
[
1785715200,
1391.0,
1399.9,
1386.2,
1394.4,
6412337,
],
[
1785801600,
1394.4,
1402.0,
1388.0,
1398.1,
5530124,
],
],
}
return {
'status_code': 200,
'data': body,
}
class DayBarsStampedInUtcExample:
"""Fetches and parses a window with bars and a window with none.
Attributes:
api (RecordedFyersAPI): The stand-in Fyers API.
candles (FyersCandles): 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 = RecordedFyersAPI()
self.candles = object.__new__(FyersCandles)
self.candles._api = self.api
def fetch_and_print(self, start_date, end_date):
"""Fetches one window of RELIANCE day bars and prints the request and the bars.
Args:
start_date (datetime.date): The first day of the window.
end_date (datetime.date): The last day of the window.
Returns:
None: This method returns nothing.
"""
payload = self.candles.fetch_candles('NSE:RELIANCE-EQ', 'day', start_date, end_date)
url, parameters = self.api.requests[-1]
print(f'Requested {url}')
print(f' parameters: {parameters}')
print(f' status: {payload["s"]}')
bars = self.candles.parse_response(payload, 'day')
print(f' bars: {len(bars)}')
for index, bar in enumerate(bars):
bar_time, open_price, high, low, close, volume, open_interest = bar
sent_epoch = payload['candles'][index][0]
sent_time = datetime.datetime.fromtimestamp(sent_epoch, datetime.timezone.utc)
print(f' sent {sent_time.isoformat()}, stored {bar_time.isoformat()}: open {open_price} high {high} low {low} close {close} volume {volume} oi {open_interest}')
def run(self):
"""Fetches the window with bars, then the empty one.
Returns:
None: This method returns nothing.
"""
self.fetch_and_print(datetime.date(2026, 8, 1), datetime.date(2026, 8, 4))
self.fetch_and_print(datetime.date(1995, 1, 1), datetime.date(1995, 12, 31))
if __name__ == '__main__':
DayBarsStampedInUtcExample().run()
It prints:
Requested https://api-t1.fyers.in/data/history
parameters: {'symbol': 'NSE:RELIANCE-EQ', 'resolution': '1D', 'date_format': '1', 'range_from': '2026-08-01', 'range_to': '2026-08-04', 'cont_flag': '1', 'oi_flag': '1'}
status: ok
bars: 2
sent 2026-08-03T00:00:00+00:00, stored 2026-08-03T00:00:00+05:30: open 1391.0 high 1399.9 low 1386.2 close 1394.4 volume 6412337 oi None
sent 2026-08-04T00:00:00+00:00, stored 2026-08-04T00:00:00+05:30: open 1394.4 high 1402.0 low 1388.0 close 1398.1 volume 5530124 oi None
Requested https://api-t1.fyers.in/data/history
parameters: {'symbol': 'NSE:RELIANCE-EQ', 'resolution': '1D', 'date_format': '1', 'range_from': '1995-01-01', 'range_to': '1995-12-31', 'cont_flag': '1', 'oi_flag': '1'}
status: no_data
bars: 0
"""Lists Fyers tickers with the kind and expiry the queue needs, reads what each ticker says, and sorts Fyers' refusals into the downloader's error kinds.
Fyers names an instrument's kind with a number that is not stable across segments, and its expiry is epoch seconds, which the queue's priority cannot read. `instruments` therefore reads the kind from the ticker's ending (`FUT` for a future, `CE` or `PE` for an option) and turns the expiry into a date. The program gives the downloader a stand-in database connection whose cursor answers with four tickers. `series_context`, a classmethod that needs no downloader, says the series is matched on the ticker as `order_symbol`, with the exchange and segments the ticker implies.
The second half fetches through a stand-in for `FyersAPI` that raises the `FyersAPIException` each of five refusals produces. An invalid symbol (-300) retires the series, code -16 means the session was refused, Cloudflare's error 1015 page is a block that must stop the broker, an ordinary 429 is a throttle, and -50, Fyers' catch-all input error, is left as it is, because it means this module asked for too wide a window rather than that the instrument is dead. The downloader is built without its constructor, as in `test_runs/candle_parse.py`, so nothing connects to PostgreSQL or logs in.
Run it from the project root:
python examples/stock_brokers/instruments/historical/fyers/FyersCandles/example_2_tickers_and_refusals.py
"""
import datetime
from stock_brokers.api.fyers import (
FyersAPIException,
)
from stock_brokers.instruments.historical.base import (
CandleError,
)
from stock_brokers.instruments.historical.fyers import (
FyersCandles,
)
class TickerCursor:
"""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 (ticker, expiry in epoch seconds as text).
Returns:
None: This method returns nothing.
"""
self.rows = rows
def __enter__(self):
"""Enters the `with` block.
Returns:
TickerCursor: 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 TickerConnection:
"""A stand-in PostgreSQL connection over four rows of Fyers' instrument master.
Attributes:
rows (list): The rows every cursor answers with.
"""
def __init__(self):
"""Builds the connection with four tickers.
Returns:
None: This method returns nothing.
"""
self.rows = [
(
'NSE:RELIANCE-EQ',
'0',
),
(
'NSE:NIFTY50-INDEX',
'',
),
(
'NSE:RELIANCE26SEPFUT',
'1790676000',
),
(
'NSE:NIFTY25D3026000CE',
'1767088800',
),
]
def cursor(self):
"""Opens a cursor.
Returns:
TickerCursor: A cursor over the rows.
"""
return TickerCursor(self.rows)
def commit(self):
"""Accepts a commit.
Returns:
None: This method returns nothing.
"""
class RefusingFyersAPI:
"""A stand-in for `FyersAPI` that refuses with a chosen Fyers error.
Attributes:
code (int): The code the refusal carries.
message (str): The message the refusal carries.
"""
def __init__(self, code, message):
"""Holds the refusal to raise.
Args:
code (int): Fyers' code, or the HTTP status for a page that is not Fyers' JSON.
message (str): The message.
Returns:
None: This method returns nothing.
"""
self.code = code
self.message = message
def get(self, url, params=None):
"""Refuses a GET request.
Args:
url (str): The URL the downloader asked for.
params (dict): The query parameters it sent.
Returns:
dict: Never returns.
Raises:
FyersAPIException: Always, carrying the chosen code and message.
"""
raise FyersAPIException(code=self.code, message=self.message)
class TickersAndRefusalsExample:
"""Lists tickers, reads them, and classifies five refusals.
Attributes:
candles (FyersCandles): 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__(FyersCandles)
self.candles._connection = TickerConnection()
def show_tickers(self):
"""Lists the instruments and reads each ticker.
Returns:
None: This method returns nothing.
"""
for ticker, kind, expiry in self.candles.instruments():
context = FyersCandles.series_context(ticker)
print(f'{ticker}: kind {kind}, expiry {expiry}, matched on {context.match_on}, exchange {context.exchange}, {len(context.segments)} segments to search')
def show_refusals(self):
"""Fetches through five refusing stand-ins and prints what each became.
Returns:
None: This method returns nothing.
"""
refusals = [
(
-300,
'Invalid symbol provided',
),
(
-16,
'Could not authenticate the user',
),
(
429,
'error code: 1015 - the owner of this website has banned you temporarily',
),
(
429,
'request limit reached',
),
(
-50,
'Date range cannot exceed 366 days for 1D, 1W and 1M',
),
]
for code, message in refusals:
self.candles._api = RefusingFyersAPI(code, message)
try:
self.candles.fetch_candles(
'NSE:RELIANCE-EQ',
'day',
datetime.date(2025, 1, 1),
datetime.date(2026, 9, 11),
)
outcome = 'answered'
except CandleError as error:
outcome = type(error).__name__
except FyersAPIException as error:
outcome = f'left as {type(error).__name__}'
print(f'{code} "{message}": {outcome}')
def run(self):
"""Runs both parts in turn.
Returns:
None: This method returns nothing.
"""
self.show_tickers()
self.show_refusals()
if __name__ == '__main__':
TickersAndRefusalsExample().run()
It prints:
NSE:RELIANCE-EQ: kind EQ, expiry None, matched on order_symbol, exchange nse, 6 segments to search
NSE:NIFTY50-INDEX: kind EQ, expiry None, matched on order_symbol, exchange nse, 4 segments to search
NSE:RELIANCE26SEPFUT: kind FUT, expiry 2026-09-29, matched on order_symbol, exchange nse, 16 segments to search
NSE:NIFTY25D3026000CE: kind OPT, expiry 2025-12-30, matched on order_symbol, exchange nse, 16 segments to search
-300 "Invalid symbol provided": CandleInstrumentUnknown
-16 "Could not authenticate the user": CandleAuthenticationError
429 "error code: 1015 - the owner of this website has banned you temporarily": CandleBlocked
429 "request limit reached": CandleThrottled
-50 "Date range cannot exceed 366 days for 1D, 1W and 1M": left as FyersAPIException
"""Shows when the Fyers candle download pauses for trading hours, and that a stop request ends the pause at once.
Every Fyers request counts toward the app's 200 a minute, shared with the pollers and orders, so the history download stays out of trading hours: 09:00 to 23:55 India time, Monday to Friday, from the equity pre-open to the latest close of MCX's evening session. Weekends are free all day, and `daily_cap` raises the cap there from 45,000 requests to 100,000. `seconds_until_trading_ends` says how long the pause lasts from a given moment, and `claim` waits it out before claiming a series, on the stop event `run` remembers, so `systemctl stop` does not have to wait. This program builds the downloader without logging in or opening a database, which is enough for these two methods.
Notice that 08:59 and 23:55 on a Wednesday, and noon on a Saturday, have no pause, and that the stopped claim returns None within a fraction of a second.
Run it from the project root:
python examples/stock_brokers/instruments/historical/fyers/FyersCandles/example_3_trading_hours_pause.py
"""
import datetime
import logging
import threading
import time
from stock_brokers.instruments.historical.base import INDIA_TIMEZONE
from stock_brokers.instruments.historical.fyers import FyersCandles
class ClockedFyersCandles(FyersCandles):
"""The Fyers downloader built without a session or a database, with its clock fixed at noon on Wednesday 2026-09-30."""
def __init__(self):
"""Builds the downloader with only a logger.
Returns:
None: This method returns nothing.
"""
self._logger = logging.getLogger('example')
self._logger.addHandler(logging.NullHandler())
self._logger.propagate = False
def seconds_until_trading_ends(self, now=None):
"""The pause from noon on Wednesday 2026-09-30, whatever the real time.
Args:
now (datetime.datetime | None): Ignored.
Returns:
float: The seconds until 23:55.
"""
del now
noon = datetime.datetime(2026, 9, 30, 12, 0, tzinfo=INDIA_TIMEZONE)
return super().seconds_until_trading_ends(noon)
class TradingHoursPauseExample:
"""Prints the pause at several moments and stops a paused claim.
Attributes:
candles (ClockedFyersCandles): The downloader.
"""
def __init__(self):
"""Builds the downloader.
Returns:
None: This method returns nothing.
"""
self.candles = ClockedFyersCandles()
def run(self):
"""Prints the pause at six moments, then stops a run whose claim is paused.
Returns:
None: This method returns nothing.
"""
moments = [
('Wednesday 08:59', datetime.datetime(2026, 9, 30, 8, 59, tzinfo=INDIA_TIMEZONE)),
('Wednesday 09:00', datetime.datetime(2026, 9, 30, 9, 0, tzinfo=INDIA_TIMEZONE)),
('Wednesday 15:30', datetime.datetime(2026, 9, 30, 15, 30, tzinfo=INDIA_TIMEZONE)),
('Wednesday 23:55', datetime.datetime(2026, 9, 30, 23, 55, tzinfo=INDIA_TIMEZONE)),
('Saturday 12:00', datetime.datetime(2026, 10, 3, 12, 0, tzinfo=INDIA_TIMEZONE)),
('Sunday 20:00', datetime.datetime(2026, 10, 4, 20, 0, tzinfo=INDIA_TIMEZONE)),
]
for label, moment in moments:
pause = FyersCandles.seconds_until_trading_ends(self.candles, moment)
print(f'{label}: pause for {pause / 3600:.2f} hours')
for label, moment in [moments[2], moments[4]]:
print(f'Daily cap on {label}: {self.candles.daily_cap(moment)}')
stop_event = threading.Event()
stop_event.set()
visits, bars = self.candles.run(stop_event=stop_event)
print(f'A run asked to stop before it starts: {visits} windows, {bars} bars')
stop_event.clear()
threading.Timer(0.2, stop_event.set).start()
started = time.monotonic()
claimed = self.candles.claim()
waited = time.monotonic() - started
print(f'A claim at noon, stopped after 0.2 s: {claimed}, back within a second: {waited < 1}')
if __name__ == '__main__':
TradingHoursPauseExample().run()
It prints:
Wednesday 08:59: pause for 0.00 hours
Wednesday 09:00: pause for 14.92 hours
Wednesday 15:30: pause for 8.42 hours
Wednesday 23:55: pause for 0.00 hours
Saturday 12:00: pause for 0.00 hours
Sunday 20:00: pause for 0.00 hours
Daily cap on Wednesday 15:30: 45000
Daily cap on Saturday 12:00: 100000
A run asked to stop before it starts: 0 windows, 0 bars
A claim at noon, stopped after 0.2 s: None, back within a second: True
Methods:
| Name | Description |
|---|---|
run |
Work the queue as the base class does, remembering the stop event so the trading-hours pause can end at once when the process is asked to stop. |
seconds_until_trading_ends |
How long until the markets stop trading, or zero outside trading hours. |
daily_cap |
The daily request cap for a day: |
claim |
Claim the next series, first waiting out trading hours without holding any series, and apply the day's cap. |
instruments |
Every ticker Fyers has ever published, with the fields the queue's priority needs. |
series_context |
Read the exchange and segment out of a Fyers ticker. |
fetch_candles |
Fetch one window of Fyers candles. |
parse_response |
Turn Fyers' candle list into bars. |
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. |
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. |
summary |
How far the download has got, for a status display. |
close |
Close the database connection. |
Source code in stock_brokers/instruments/historical/fyers.py
run
¶
Work the queue as the base class does, remembering the stop event so the trading-hours pause can end at once when the process is asked to stop.
stop_eventis athreading.Eventthat ends the loop when set.deadline_secondsends the loop after this long.
Example usage
Source code in stock_brokers/instruments/historical/fyers.py
seconds_until_trading_ends
¶
How long until the markets stop trading, or zero outside trading hours.
Trading hours are 09:00 to 23:55 India time, Monday to Friday: from the equity pre-open to the latest close of MCX's evening session. The download stays out of them so its requests never compete with the pollers and orders for Fyers' 200 a minute. Exchange holidays are treated as trading days.
nowis the moment to reckon from, or None for now in India.
Example usage
pause = FyersCandles.seconds_until_trading_ends(self.candles, moment)
Source code in stock_brokers/instruments/historical/fyers.py
daily_cap
¶
The daily request cap for a day: REQUESTS_PER_DAY on a trading day, and WEEKEND_REQUESTS_PER_DAY at the weekend, when the download runs all day, as the user asked on 2026-09-30.
nowis the moment to reckon from, or None for now in India.
Example usage
print(f'Daily cap on {label}: {self.candles.daily_cap(moment)}')
Source code in stock_brokers/instruments/historical/fyers.py
claim
¶
Claim the next series, first waiting out trading hours without holding any series, and apply the day's cap.
Returns None, which ends the run, when the process is asked to stop during the wait.
Example usage
Source code in stock_brokers/instruments/historical/fyers.py
instruments
¶
Every ticker Fyers has ever published, with the fields the queue's priority needs.
Two translations happen here rather than in priority_for, because both are Fyers'
vocabulary rather than this project's.
Fyers names an instrument's kind with a number - 13 is a stock future, 15 a stock option -
and the numbers are not stable enough across segments to be worth a lookup table when the
ticker already says it plainly: it ends in FUT for a future and CE or PE for an
option. And its expiry is epoch seconds rather than a date, which priority_for cannot
read, so it is converted here. Without the conversion every expired contract would be
queued as though it were live, ahead of cash intraday bars that are actually wanted.
Example usage
Source code in stock_brokers/instruments/historical/fyers.py
_expiry_date
staticmethod
¶
Turn Fyers' epoch expiry into a date, or None when the instrument does not expire.
expiryis theexpiry_datecolumn, epoch seconds as text.
Source code in stock_brokers/instruments/historical/fyers.py
series_context
classmethod
¶
Read the exchange and segment out of a Fyers ticker.
The price table names an instrument by its ticker, while the mapping keys Fyers by its
numeric fytoken. The ticker is kept in the mapping as order_symbol, so that is what the
series is matched on.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
identifier
|
str
|
For example "NSE:RELIANCE-EQ" or "NSE:NIFTY50-INDEX". |
required |
Returns:
| Type | Description |
|---|---|
SeriesContext
|
The ticker, to be matched on |
Example usage
Source code in stock_brokers/instruments/historical/fyers.py
fetch_candles
¶
Fetch one window of Fyers candles.
tokenis the Fyers ticker, such asNSE:RELIANCE-EQ.intervalis the stored interval name.start_dateandend_datebound the window, inclusive.
Example usage
payload = self.candles.fetch_candles('NSE:RELIANCE-EQ', 'day', start_date, end_date)
self.candles.fetch_candles(
'NSE:RELIANCE-EQ',
'day',
datetime.date(2025, 1, 1),
datetime.date(2026, 9, 11),
)
Source code in stock_brokers/instruments/historical/fyers.py
_classify
staticmethod
¶
Decide what a Fyers refusal means for the series being downloaded.
Fyers answers an unknown ticker with code -300, "Invalid symbol provided", which is the one that retires a series. Code -50 is its catch-all input error and is what a window wider than the cap returns, so it is a fault in this module rather than a dead instrument and is left to back off.
The session codes are matched on the number Fyers sends, not on the wording. Cloudflare's block page is recognised before any 429, since it is served as one and must stop the broker rather than slow it down.
exceptionis what the API call raised.
Source code in stock_brokers/instruments/historical/fyers.py
parse_response
¶
Turn Fyers' candle list into bars.
An empty window is an HTTP 200 carrying s of no_data, which is read here and answered
with no bars. A row is [epoch, open, high, low, close, volume] for a cash instrument and
carries open interest as a seventh element for a derivative, so it is read by length.
payloadis the decoded response body.intervalis the stored interval name.
Example usage
Source code in stock_brokers/instruments/historical/fyers.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
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
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