factors
¶
Derive split, bonus and demerger factors from Yahoo Finance and the stored raw prices.
For each instrument stored unadjusted, the ratio q = Yahoo Close / raw close is taken on every day
both have. Yahoo's Close is adjusted and the raw close is not, so q is 1 after the last corporate
action and steps down at each action going back in time: RELIANCE's q is 1 from 2024-10-28, 0.5
before that bonus, and 0.4615 before the 2023-07-20 demerger of Jio Financial, whose factor of about
0.923 Yahoo builds into Close without ever listing it as an event.
Steps. q is walked from the newest day back. A move away from the current level counts as a step
only when it holds for PERSISTENCE trading days, which passes over Yahoo's one-day bad prints
(2025-03-18 across much of the market; NIFTYBEES for two days in December 2019). The tolerance widens
for low prices, where Yahoo's rounding of the adjusted price to two decimals is a larger share of it.
A step's size is the ratio of the median q on either side of it.
Classification. Each step is matched to a listed Yahoo event within MATCH_WINDOW trading days:
- a step equal to the event's factor confirms a
splitat the exact ratio; - a step below it confirms the split and adds a provisional
unclassifiedrow for the remainder; - a step with no event is
unclassifiedand provisional, with a note of whether the raw close fell by about the same amount. Demergers land here, but so do rights issues - Yahoo adjusts Close for those too - and inconsistencies in either series; across the first full build, 426 such steps met the old test for a demerger, most of them rights issues (UPL, SUZLON, PNBHOUSING) or whole ratios left over from a double adjustment (NAZARA's 0.5, RHETAN's 0.1). A person confirms a demerger.
Only listed split events are therefore confirmed automatically.
A listed event with no step anywhere inside the stored history is rejected, because applying it
would be wrong for these stored prices whatever the reason. Usually the stored prices already include
it: INDIAGLYCO's 1:1 bonus of 2025-08-12 was real, and its BSE series steps there, but flattrade's NSE
series for it was served already halved before that date, so on NSE there is nothing left to apply.
Events from before the stored history cannot be checked and are stored provisional.
A rebuild replaces the derived rows for an instrument: derived rows it no longer produces are deleted, so a step that moves - when a hole in the bars is filled - is not applied twice.
Raw gaps. An overnight move in the raw close beyond RAW_GAP with no factor near it is recorded
as a provisional raw_gap row for a person to look at, with the evidence gathered: the ratio, the
nearest simple fraction, whether the series changed token that day, and the factor dhan's re-adjusted
history implies. This is how an action Yahoo has not caught up with surfaces - INDIAGLYCO on 2026-09-02.
Rows a person set (source = 'manual') are never touched, and rejected rows are never revived.
Classes:
| Name | Description |
|---|---|
FactorBuilder |
Builds unified.adjustment_factors for the instruments stored unadjusted. |
Functions:
| Name | Description |
|---|---|
tolerance |
How far q may wander from its level without being a move, for a given price. |
find_steps |
The persistent level changes in q, newest first. |
exact_ratio |
A split ratio as shares after and before. |
FactorBuilder
¶
Builds unified.adjustment_factors for the instruments stored unadjusted.
Attributes:
| Name | Type | Description |
|---|---|---|
connection |
The psycopg2 connection. |
|
yahoo |
YahooClient
|
Paced Yahoo access. |
counts |
dict
|
What was derived, for the run summary. |
Prepare a build.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
yahoo
|
YahooClient | None
|
A client to share, or None to build one. |
None
|
Returns:
| Type | Description |
|---|---|
None
|
This function returns nothing. |
Example programs
"""Builds RELIANCE's adjustment factor for its 1:1 bonus of 2024-10-28, confirmed against the event Yahoo lists.
`FactorBuilder` derives split, bonus and demerger factors for every instrument stored unadjusted. For each one it reads the raw daily closes, fetches Yahoo's history, and takes the ratio q of Yahoo's close to the raw close on every day both have. Yahoo's close is adjusted and the raw close is not, so q is 1 after the last corporate action and steps down at each one going back in time. A step that matches an event Yahoo lists is a confirmed split at the exact ratio; here q is 0.5 before the bonus and 1 from it, and Yahoo lists the bonus as a ratio of 2.0, so the builder stores a confirmed 2-for-1 factor that halves earlier prices and doubles earlier volumes. The 50% overnight fall in the raw close is explained by that factor, so it is not reported as a raw gap.
The builder's constructor opens a PostgreSQL connection through `get_postgres`, and the example runner points PostgreSQL at a closed port, so the program replaces `get_postgres` in the factors module with a stand-in that returns an in-memory connection. That connection answers the builder's queries with 34 recorded-looking raw closes for October and November 2024 and records every row written. The builder accepts its Yahoo client as an argument, so a stand-in client is passed in; it answers with closes equal to the raw ones times q, rounded to the paisa as Yahoo rounds them, and never touches the network. The program first calls `instruments` and `raw_daily` to show what the build reads, then runs `build`.
Run it from the project root:
python examples/stock_brokers/instruments/historical/utilities/unified/factors/FactorBuilder/example_1_a_bonus_confirmed_by_yahoo.py
"""
import datetime
from stock_brokers.instruments.historical.utilities.unified import factors
from stock_brokers.instruments.historical.utilities.unified.factors import (
FactorBuilder,
)
BONUS_DATE = datetime.date(2024, 10, 28)
class RawHistory:
"""RELIANCE's raw daily closes around its bonus, as the unified price table would hold them.
Attributes:
days (list): The trading dates, oldest first.
closes (list): The raw close on each date.
"""
def __init__(self):
"""Builds the weekdays from 2024-10-01 to 2024-11-15 with closes near 2,700 before the bonus and 1,350 from it.
Returns:
None: This method returns nothing.
"""
self.days = []
self.closes = []
day = datetime.date(2024, 10, 1)
position = 0
while day <= datetime.date(2024, 11, 15):
if day.weekday() < 5:
wiggle = (position % 4) * 6.5
if day < BONUS_DATE:
self.closes.append(2700.0 + wiggle)
else:
self.closes.append(1350.0 + wiggle / 2)
self.days.append(day)
position += 1
day += datetime.timedelta(days=1)
class StandInYahooClient:
"""A stand-in for `YahooClient` that answers with closes adjusted for the bonus.
Attributes:
raw (RawHistory): The raw history the adjusted closes are made from.
tickers (list): Every ticker asked for.
"""
def __init__(self, raw):
"""Remembers the raw history.
Args:
raw (RawHistory): The raw history the adjusted closes are made from.
Returns:
None: This method returns nothing.
"""
self.raw = raw
self.tickers = []
def history(self, ticker):
"""Answers Yahoo's closes and listed events for a ticker.
Args:
ticker (str): The Yahoo ticker.
Returns:
tuple: (closes by date, events by date).
"""
self.tickers.append(ticker)
closes = {}
for day, raw_close in zip(self.raw.days, self.raw.closes):
if day < BONUS_DATE:
closes[day] = round(raw_close * 0.5, 2)
else:
closes[day] = raw_close
events = {
BONUS_DATE: 2.0,
}
return closes, events
class StandInCursor:
"""A stand-in for a psycopg2 cursor that hands every statement to its connection.
Attributes:
connection (StandInConnection): The connection that answers the statements.
result (list): The rows the last statement produced.
rowcount (int): How many rows the last statement changed.
"""
def __init__(self, connection):
"""Builds a cursor over the stand-in connection.
Args:
connection (StandInConnection): The connection that answers the statements.
Returns:
None: This method returns nothing.
"""
self.connection = connection
self.result = []
self.rowcount = 0
def __enter__(self):
"""Opens the cursor as a context manager.
Returns:
StandInCursor: This cursor.
"""
return self
def __exit__(self, exception_type, exception, traceback):
"""Closes the cursor, letting any exception carry on.
Args:
exception_type (type | None): The class of an exception raised inside the block.
exception (BaseException | None): The exception raised inside the block.
traceback (types.TracebackType | None): Where it was raised.
Returns:
bool: Always False, so an exception is not swallowed.
"""
return False
def execute(self, statement, parameters):
"""Runs one statement against the stand-in connection.
Args:
statement (str): The SQL text.
parameters (tuple | dict): The statement's parameters.
Returns:
None: This method returns nothing.
"""
self.result, self.rowcount = self.connection.answer(statement, parameters)
def fetchone(self):
"""The first row the last statement produced.
Returns:
tuple | None: The row, or None when there was none.
"""
if not self.result:
return None
return self.result[0]
def fetchall(self):
"""Every row the last statement produced.
Returns:
list: The rows.
"""
return self.result
class StandInConnection:
"""A stand-in for a psycopg2 connection over RELIANCE's raw history.
Attributes:
raw (RawHistory): The raw history the price table query returns.
factors_written (list): Every adjustment factor row inserted.
fetches_written (list): Every Yahoo fetch state row written.
commits (int): How many times the builder committed.
"""
def __init__(self, raw):
"""Starts with nothing written.
Args:
raw (RawHistory): The raw history the price table query returns.
Returns:
None: This method returns nothing.
"""
self.raw = raw
self.factors_written = []
self.fetches_written = []
self.commits = 0
def cursor(self):
"""Opens a stand-in cursor.
Returns:
StandInCursor: The cursor.
"""
return StandInCursor(self)
def answer(self, statement, parameters):
"""Answers one of the builder's statements.
Args:
statement (str): The SQL text.
parameters (tuple | dict): The statement's parameters.
Returns:
tuple: (rows, rows changed).
"""
if 'm.symbol not like' in statement:
row = (
'RELIANCE-NSE',
'nse',
'RELIANCE',
None,
False,
)
return [row], 0
if 'close, source_id' in statement:
rows = []
for day, close in zip(self.raw.days, self.raw.closes):
row = (
day,
close,
501,
)
rows.append(row)
return rows, 0
if "source = 'manual'" in statement and 'select ex_date' in statement:
return [], 0
if 'delete from' in statement:
return [], 0
if 'insert into unified.adjustment_factors' in statement:
self.factors_written.append(parameters)
return [], 1
if 'insert into unified.yahoo_fetch_state' in statement:
self.fetches_written.append(parameters)
return [], 1
raise ValueError(f'The stand-in does not know this statement: {statement[:60]}')
def commit(self):
"""Counts a commit.
Returns:
None: This method returns nothing.
"""
self.commits += 1
def rollback(self):
"""Ends a read transaction, which the builder does before each Yahoo request.
Returns:
None: This method returns nothing.
"""
class StandInPostgres:
"""Hands out the stand-in connection in the place of `get_postgres`.
Attributes:
connection (StandInConnection): The connection handed out.
"""
def __init__(self, connection):
"""Remembers the connection to hand out.
Args:
connection (StandInConnection): The connection handed out.
Returns:
None: This method returns nothing.
"""
self.connection = connection
def get_postgres(self):
"""Returns the stand-in connection.
Returns:
StandInConnection: The connection.
"""
return self.connection
class BonusConfirmedByYahooExample:
"""Runs the factor build for RELIANCE and prints what it stored.
Attributes:
connection (StandInConnection): The in-memory connection.
yahoo (StandInYahooClient): The stand-in Yahoo client.
builder (FactorBuilder): The builder being shown.
"""
def __init__(self):
"""Installs the stand-in connection and builds the builder with the stand-in Yahoo client.
Returns:
None: This method returns nothing.
"""
raw = RawHistory()
self.connection = StandInConnection(raw)
factors.get_postgres = StandInPostgres(self.connection).get_postgres
self.yahoo = StandInYahooClient(raw)
self.builder = FactorBuilder(self.yahoo)
def run(self):
"""Prints what the build reads, then builds and prints the stored rows.
Returns:
None: This method returns nothing.
"""
print(f'Instruments to build: {self.builder.instruments()}')
days, closes, source_ids = self.builder.raw_daily('RELIANCE-NSE')
print(f'Raw closes: {len(days)} days from {days[0]} to {days[-1]}, {closes[0]} on the first, {closes[-1]} on the last, source {source_ids[0]}')
counts = self.builder.build()
print(f'Build summary: {counts}')
print(f'Yahoo tickers asked for: {self.yahoo.tickers}')
for row in self.connection.factors_written:
print(f"Stored: {row['ex_date']} {row['kind']} {row['shares_after']}:{row['shares_before']} price factor {row['price_factor']} volume factor {row['volume_factor']} {row['status']} from {row['source']}")
print(f" evidence {row['evidence']}")
for fetch in self.connection.fetches_written:
print(f'Fetch state: ticker {fetch[1]} ({fetch[2]}), status {fetch[3]}, Yahoo history {fetch[5]} to {fetch[6]}, last event {fetch[7]}')
print(f'Commits: {self.connection.commits}')
if __name__ == '__main__':
BonusConfirmedByYahooExample().run()
It prints:
Instruments to build: [('RELIANCE-NSE', 'nse', 'RELIANCE', None, False)]
Raw closes: 34 days from 2024-10-01 to 2024-11-15, 2700.0 on the first, 1353.25 on the last, source 501
Build summary: {'stale_deleted': 0, 'split_confirmed': 1, 'ok': 1}
Yahoo tickers asked for: ['RELIANCE.NS']
Stored: 2024-10-28 split 2:1 price factor 0.5 volume factor 2.0 confirmed from yahoo_split_event
evidence {"q_before": 0.5, "q_after": 1.0, "raw_close_before": 2713.0, "raw_close_after": 1359.75, "residual": 1.0}
Fetch state: ticker RELIANCE.NS (ticker), status ok, Yahoo history 2024-10-01 to 2024-11-15, last event 2024-10-28
Commits: 2
"""Shows what `FactorBuilder` does with evidence it cannot confirm by itself: an unlisted step, a listed event with no step, a raw gap, and a ticker that does not fit.
Only a step that matches a split Yahoo lists is confirmed automatically. This program calls the builder's steps one at a time to show the rest:
- `derive` on a demerger-shaped history: q steps from 0.923 to 1 on 2023-07-20 with no listed event, and the raw close falls by about the same amount, so the step is stored as a provisional `unclassified` row that notes the raw price fell alike, for a person to confirm. A split Yahoo lists on 2023-07-10, well inside the history but with no step in q, is `rejected`, because the stored prices already move with Yahoo's across it.
- `raw_gaps` on INDIAGLYCO's raw closes, which halve overnight on 2026-09-02 with nothing derived nearby: the move becomes a provisional `raw_gap` row carrying the evidence gathered, including that the series changed token that day and the factor Dhan's re-adjusted history implies, which `dhan_factor` reads.
- `store` writing those rows, except for a date a person has already decided by hand, and `record_fetch` noting a failed fetch.
- `build_one` for a BSE share whose every ticker form is a stub, which is recorded as `empty`, and for one whose newest q is 0.5 rather than 1, which means the ticker is not this instrument and is recorded as `mismatch`.
The constructor opens PostgreSQL through `get_postgres`, which the example runner points at a closed port, so the program replaces `get_postgres` in the factors module with a stand-in that returns an in-memory connection answering the builder's queries with canned rows and recording what is written. The Yahoo client is passed in as a stand-in, so the network is never touched.
Run it from the project root:
python examples/stock_brokers/instruments/historical/utilities/unified/factors/FactorBuilder/example_2_steps_yahoo_does_not_explain.py
"""
import datetime
from stock_brokers.instruments.historical.utilities.unified import factors
from stock_brokers.instruments.historical.utilities.unified.factors import (
FactorBuilder,
)
DEMERGER_DATE = datetime.date(2023, 7, 20)
GAP_DATE = datetime.date(2026, 9, 2)
class StandInYahooClient:
"""A stand-in for `YahooClient` that answers a stub for every BSE form and a mismatched history for one NSE ticker.
Attributes:
tickers (list): Every ticker asked for.
"""
def __init__(self):
"""Starts with no tickers asked for.
Returns:
None: This method returns nothing.
"""
self.tickers = []
def history(self, ticker):
"""Answers Yahoo's closes and listed events for a ticker.
Args:
ticker (str): The Yahoo ticker.
Returns:
tuple: (closes by date, events by date).
"""
self.tickers.append(ticker)
closes = {}
if ticker == 'WRONGCO.NS':
day = datetime.date(2026, 9, 1)
for position in range(12):
closes[day + datetime.timedelta(days=position)] = 50.0
if ticker == 'STUBCO.BO':
closes[datetime.date(2026, 9, 1)] = 100.0
return closes, {}
class StandInCursor:
"""A stand-in for a psycopg2 cursor that hands every statement to its connection.
Attributes:
connection (StandInConnection): The connection that answers the statements.
result (list): The rows the last statement produced.
rowcount (int): How many rows the last statement changed.
"""
def __init__(self, connection):
"""Builds a cursor over the stand-in connection.
Args:
connection (StandInConnection): The connection that answers the statements.
Returns:
None: This method returns nothing.
"""
self.connection = connection
self.result = []
self.rowcount = 0
def __enter__(self):
"""Opens the cursor as a context manager.
Returns:
StandInCursor: This cursor.
"""
return self
def __exit__(self, exception_type, exception, traceback):
"""Closes the cursor, letting any exception carry on.
Args:
exception_type (type | None): The class of an exception raised inside the block.
exception (BaseException | None): The exception raised inside the block.
traceback (types.TracebackType | None): Where it was raised.
Returns:
bool: Always False, so an exception is not swallowed.
"""
return False
def execute(self, statement, parameters):
"""Runs one statement against the stand-in connection.
Args:
statement (str): The SQL text.
parameters (tuple | dict): The statement's parameters.
Returns:
None: This method returns nothing.
"""
self.result, self.rowcount = self.connection.answer(statement, parameters)
def fetchone(self):
"""The first row the last statement produced.
Returns:
tuple | None: The row, or None when there was none.
"""
if not self.result:
return None
return self.result[0]
def fetchall(self):
"""Every row the last statement produced.
Returns:
list: The rows.
"""
return self.result
class StandInConnection:
"""A stand-in for a psycopg2 connection with raw closes for two instruments and one Dhan bar.
Attributes:
raw (dict): Instrument id to its raw (date, close, source id) rows.
factors_written (list): Every adjustment factor row inserted.
fetches_written (list): Every Yahoo fetch state row written.
"""
def __init__(self):
"""Fills in twelve days of raw closes for two instruments.
Returns:
None: This method returns nothing.
"""
self.raw = {}
self.factors_written = []
self.fetches_written = []
instrument_ids = [
'STUBCO-BSE',
'WRONGCO-NSE',
]
for instrument_id in instrument_ids:
rows = []
day = datetime.date(2026, 9, 1)
for position in range(12):
row = (
day + datetime.timedelta(days=position),
100.0,
601,
)
rows.append(row)
self.raw[instrument_id] = rows
def cursor(self):
"""Opens a stand-in cursor.
Returns:
StandInCursor: The cursor.
"""
return StandInCursor(self)
def answer(self, statement, parameters):
"""Answers one of the builder's statements.
Args:
statement (str): The SQL text.
parameters (tuple | dict): The statement's parameters.
Returns:
tuple: (rows, rows changed).
"""
if 'close, source_id' in statement:
return self.raw[parameters[0]], 0
if 'join dhan.price_history' in statement:
row = (
242.5,
)
return [row], 0
if "source = 'manual'" in statement and 'select ex_date' in statement:
row = (
datetime.date(2023, 7, 10),
)
return [row], 0
if 'delete from' in statement:
return [], 2
if 'insert into unified.adjustment_factors' in statement:
self.factors_written.append(parameters)
return [], 1
if 'insert into unified.yahoo_fetch_state' in statement:
self.fetches_written.append(parameters)
return [], 1
raise ValueError(f'The stand-in does not know this statement: {statement[:60]}')
def commit(self):
"""Accepts a commit.
Returns:
None: This method returns nothing.
"""
def rollback(self):
"""Accepts a rollback.
Returns:
None: This method returns nothing.
"""
class StandInPostgres:
"""Hands out the stand-in connection in the place of `get_postgres`.
Attributes:
connection (StandInConnection): The connection handed out.
"""
def __init__(self, connection):
"""Remembers the connection to hand out.
Args:
connection (StandInConnection): The connection handed out.
Returns:
None: This method returns nothing.
"""
self.connection = connection
def get_postgres(self):
"""Returns the stand-in connection.
Returns:
StandInConnection: The connection.
"""
return self.connection
class StepsYahooDoesNotExplainExample:
"""Calls the builder's steps one at a time and prints what each decides.
Attributes:
connection (StandInConnection): The in-memory connection.
yahoo (StandInYahooClient): The stand-in Yahoo client.
builder (FactorBuilder): The builder being shown.
"""
def __init__(self):
"""Installs the stand-in connection and builds the builder.
Returns:
None: This method returns nothing.
"""
self.connection = StandInConnection()
factors.get_postgres = StandInPostgres(self.connection).get_postgres
self.yahoo = StandInYahooClient()
self.builder = FactorBuilder(self.yahoo)
def weekdays(self, first_day, count):
"""Lists a number of weekdays from a date.
Args:
first_day (datetime.date): The first date.
count (int): How many weekdays to list.
Returns:
list: The dates.
"""
days = []
day = first_day
while len(days) < count:
if day.weekday() < 5:
days.append(day)
day += datetime.timedelta(days=1)
return days
def describe(self, row):
"""Prints one factor row on one line, with its evidence below.
Args:
row (dict): The row.
Returns:
None: This method returns nothing.
"""
print(f" {row['ex_date']} {row['kind']} {row['status']} from {row['source']}, price factor {round(row['price_factor'], 4)}")
print(f" evidence {row['evidence']}")
def derive_demerger(self):
"""Derives rows for a demerger-shaped history with one listed event that shows no step.
Returns:
list: The derived rows.
"""
days = self.weekdays(datetime.date(2023, 6, 26), 30)
raw_closes = []
ratios = []
for day in days:
if day < DEMERGER_DATE:
raw_closes.append(2600.0)
ratios.append(0.923)
else:
raw_closes.append(2400.0)
ratios.append(1.0)
events = {
datetime.date(2023, 7, 10): 2.0,
}
return self.builder.derive('DEMERGE-NSE', 'DEMERGE.NS', days, raw_closes, ratios, events)
def find_raw_gap(self):
"""Looks for overnight raw moves nothing explains in INDIAGLYCO's closes.
Returns:
list: The raw gap rows.
"""
days = self.weekdays(datetime.date(2026, 8, 24), 12)
raw_closes = []
source_ids = []
for day in days:
if day < GAP_DATE:
raw_closes.append(485.0)
source_ids.append(701)
else:
raw_closes.append(242.5)
source_ids.append(702)
return self.builder.raw_gaps('INDIAGLYCO-NSE', 'nse', days, raw_closes, source_ids, [])
def run(self):
"""Walks through each step and prints what it decided and wrote.
Returns:
None: This method returns nothing.
"""
derived = self.derive_demerger()
print('derive, for a demerger-shaped history:')
for row in derived:
self.describe(row)
gaps = self.find_raw_gap()
print('raw_gaps, for INDIAGLYCO:')
for row in gaps:
self.describe(row)
dhan = self.builder.dhan_factor('INDIAGLYCO-NSE', 'nse', datetime.date(2026, 9, 1), 485.0)
print(f'dhan_factor on 2026-09-01: {dhan}')
self.builder.store('DEMERGE-NSE', derived)
print(f'Rows stored: {len(self.connection.factors_written)}, the manually decided 2023-07-10 left alone')
self.builder.record_fetch('INDIAGLYCO-NSE', None, None, 'error', 'Yahoo answered 404', None, None, None)
self.builder.build_one('STUBCO-BSE', 'bse', 'STUBCO', '599999', False)
self.builder.build_one('WRONGCO-NSE', 'nse', 'WRONGCO', None)
print(f'Yahoo tickers asked for: {self.yahoo.tickers}')
for fetch in self.connection.fetches_written:
print(f'Fetch state for {fetch[0]}: ticker {fetch[1]}, status {fetch[3]}, error {fetch[4]}')
self.builder.count('checked_by_hand')
print(f'Counts: {self.builder.counts}')
if __name__ == '__main__':
StepsYahooDoesNotExplainExample().run()
It prints:
derive, for a demerger-shaped history:
2023-07-20 unclassified provisional from yahoo_close_ratio, price factor 0.923
evidence {'q_before': 0.923, 'q_after': 1.0, 'raw_close_before': 2600.0, 'raw_close_after': 2400.0, 'raw_move': 0.923077, 'raw_price_fell_alike': True}
2023-07-10 split rejected from yahoo_split_event, price factor 0.5
evidence {'note': "listed by Yahoo, but Yahoo's Close and the stored prices move together across it: either there was no action, or the stored prices already include it"}
raw_gaps, for INDIAGLYCO:
2026-09-02 unclassified provisional from raw_gap, price factor 0.5
evidence {'raw_close_before': 485.0, 'raw_close_after': 242.5, 'move': 0.5, 'nearest_fraction': '1/2', 'token_changed': True, 'dhan_factor': 0.5}
dhan_factor on 2026-09-01: 0.5
Rows stored: 1, the manually decided 2023-07-10 left alone
Yahoo tickers asked for: ['STUBCO.BO', '599999.BO', 'WRONGCO.NS']
Fetch state for INDIAGLYCO-NSE: ticker None, status error, error Yahoo answered 404
Fetch state for STUBCO-BSE: ticker None, status empty, error no ticker form covers the raw history
Fetch state for WRONGCO-NSE: ticker WRONGCO.NS, status mismatch, error latest Yahoo/raw ratio 0.5000
Counts: {'stale_deleted': 2, 'unclassified_provisional': 1, 'empty': 1, 'mismatch': 1, 'checked_by_hand': 1}
Methods:
| Name | Description |
|---|---|
count |
Add one to a summary count. |
instruments |
The instruments with unadjusted daily bars, and what is needed to find them on Yahoo. |
raw_daily |
One instrument's raw daily closes, and the source each came from. |
build |
Fetch, derive and store factors for each instrument. |
build_one |
Fetch one instrument from Yahoo, derive its factors and store them. |
derive |
Turn q's steps and Yahoo's events into factor rows. |
raw_gaps |
Overnight raw moves too large to be trading, with no factor near them. |
dhan_factor |
The factor dhan's adjusted history implies for a day: its close over the raw close. |
store |
Upsert derived rows, leaving manual and rejected rows as they are. |
record_fetch |
Record where the Yahoo fetch got to for an instrument. |
Source code in stock_brokers/instruments/historical/utilities/unified/factors.py
count
¶
Add one to a summary count.
Example usage
instruments
¶
The instruments with unadjusted daily bars, and what is needed to find them on Yahoo.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument_ids
|
set[str] | None
|
Restrict to these, or None for all. |
None
|
Returns:
| Type | Description |
|---|---|
list[tuple]
|
(instrument_id, exchange, symbol, scrip_code, listed_on_nse), scrip_code None for NSE. |
Example usage
print(f'Instruments to build: {self.builder.instruments()}')
Source code in stock_brokers/instruments/historical/utilities/unified/factors.py
raw_daily
¶
One instrument's raw daily closes, and the source each came from.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument_id
|
str
|
The instrument. |
required |
Returns:
| Type | Description |
|---|---|
tuple[list, list, list]
|
Dates, closes and source ids, oldest first. |
Example usage
days, closes, source_ids = self.builder.raw_daily('RELIANCE-NSE')
Source code in stock_brokers/instruments/historical/utilities/unified/factors.py
build
¶
Fetch, derive and store factors for each instrument.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument_ids
|
set[str] | None
|
Restrict to these, or None for all. |
None
|
Returns:
| Type | Description |
|---|---|
dict
|
Summary counts. |
Example usage
Source code in stock_brokers/instruments/historical/utilities/unified/factors.py
build_one
¶
Fetch one instrument from Yahoo, derive its factors and store them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument_id
|
str
|
The instrument. |
required |
exchange
|
str
|
"nse" or "bse". |
required |
symbol
|
str
|
Its trading symbol. |
required |
scrip_code
|
str | None
|
Its BSE scrip code. |
required |
listed_on_nse
|
bool
|
Whether a BSE instrument is also listed on NSE. |
False
|
Returns:
| Type | Description |
|---|---|
None
|
This function returns nothing. |
Example usage
self.builder.build_one('STUBCO-BSE', 'bse', 'STUBCO', '599999', False)
self.builder.build_one('WRONGCO-NSE', 'nse', 'WRONGCO', None)
Source code in stock_brokers/instruments/historical/utilities/unified/factors.py
derive
¶
Turn q's steps and Yahoo's events into factor rows.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument_id
|
str
|
The instrument. |
required |
ticker
|
str
|
The Yahoo ticker used. |
required |
days
|
list
|
Overlap dates, oldest first. |
required |
raw_closes
|
list[float]
|
Raw closes on those dates. |
required |
ratios
|
list[float]
|
q on those dates. |
required |
events
|
dict
|
Yahoo's listed events, date to ratio. |
required |
Returns:
| Type | Description |
|---|---|
list[dict]
|
Factor rows. |
Example usage
return self.builder.derive('DEMERGE-NSE', 'DEMERGE.NS', days, raw_closes, ratios, events)
Source code in stock_brokers/instruments/historical/utilities/unified/factors.py
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 | |
raw_gaps
¶
Overnight raw moves too large to be trading, with no factor near them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument_id
|
str
|
The instrument. |
required |
exchange
|
str
|
"nse" or "bse". |
required |
days
|
list
|
All raw dates, oldest first. |
required |
raw_closes
|
list[float]
|
Raw closes. |
required |
source_ids
|
list[int]
|
The source of each bar. |
required |
derived
|
list[dict]
|
The rows already derived, which cover gaps near them. |
required |
Returns:
| Type | Description |
|---|---|
list[dict]
|
Provisional raw_gap rows. |
Example usage
return self.builder.raw_gaps('INDIAGLYCO-NSE', 'nse', days, raw_closes, source_ids, [])
Source code in stock_brokers/instruments/historical/utilities/unified/factors.py
dhan_factor
¶
The factor dhan's adjusted history implies for a day: its close over the raw close.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument_id
|
str
|
The instrument. |
required |
exchange
|
str
|
"nse" or "bse". |
required |
day
|
date
|
The last day before the move. |
required |
raw_close
|
float
|
The raw close that day. |
required |
Returns:
| Type | Description |
|---|---|
float | None
|
The ratio, or None when dhan has no bar for the day. |
Example usage
dhan = self.builder.dhan_factor('INDIAGLYCO-NSE', 'nse', datetime.date(2026, 9, 1), 485.0)
Source code in stock_brokers/instruments/historical/utilities/unified/factors.py
store
¶
Upsert derived rows, leaving manual and rejected rows as they are.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument_id
|
str
|
The instrument. |
required |
rows
|
list[dict]
|
The derived rows. |
required |
Returns:
| Type | Description |
|---|---|
None
|
This function returns nothing. |
Example usage
Source code in stock_brokers/instruments/historical/utilities/unified/factors.py
record_fetch
¶
Record where the Yahoo fetch got to for an instrument.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument_id
|
str
|
The instrument. |
required |
ticker
|
str | None
|
The ticker used. |
required |
kind
|
str | None
|
"ticker" or "scrip_code". |
required |
status
|
str
|
"ok", "empty", "mismatch" or "error". |
required |
error
|
str | None
|
What went wrong. |
required |
history_start
|
date | None
|
Yahoo's first date. |
required |
history_end
|
date | None
|
Yahoo's last date. |
required |
last_event
|
date | None
|
Yahoo's latest listed event. |
required |
Returns:
| Type | Description |
|---|---|
None
|
This function returns nothing. |
Example usage
self.builder.record_fetch('INDIAGLYCO-NSE', None, None, 'error', 'Yahoo answered 404', None, None, None)
Source code in stock_brokers/instruments/historical/utilities/unified/factors.py
tolerance
¶
How far q may wander from its level without being a move, for a given price.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
raw_close
|
float
|
The raw close that day. |
required |
level
|
float
|
The current level of q. |
required |
Returns:
| Type | Description |
|---|---|
float
|
A relative tolerance. |
Source code in stock_brokers/instruments/historical/utilities/unified/factors.py
find_steps
¶
The persistent level changes in q, newest first.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
days
|
list[date]
|
Overlap dates, oldest first. |
required |
ratios
|
list[float]
|
q on each date. |
required |
raw_closes
|
list[float]
|
The raw close on each date. |
required |
Returns:
| Type | Description |
|---|---|
list[dict]
|
Each with "index" (of the first day at the newer level), "ex_date", "step" (older level over newer level) and "dispersion". |
Source code in stock_brokers/instruments/historical/utilities/unified/factors.py
exact_ratio
¶
A split ratio as shares after and before.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
float
|
Yahoo's ratio, for example 2.0 or 0.3. |
required |
Returns:
| Type | Description |
|---|---|
tuple[int, int]
|
(shares_after, shares_before), for example (2, 1) or (3, 10). |