resolution
¶
Resolving the tokens on broker ticks to unified instrument ids, and compiling what each needs.
A tick names its instrument in its broker's own spelling. The broker's normalizer turns that into
the token and canonical segments the broker mappings are searched with; this module does
the search, refuses to guess when the answer is not unique, and compiles an InstrumentPlan holding
everything the per-tick path needs, so that path is a single dictionary look-up.
CacheCandidateSource answers the search, over the three tier mapping cache. It covers the current mapping date
only, which is what a quote needs.
Misses are remembered for ten minutes, so a token nothing maps costs one search per ten minutes rather than one per tick, and a token mapped by the morning's download is picked up soon after.
Classes:
| Name | Description |
|---|---|
CacheCandidateSource |
Candidates from the three tier mapping cache, for the current mapping date. |
TickResolver |
Instrument plans for broker tokens, compiled once per token per mapping date. |
Functions:
| Name | Description |
|---|---|
order_symbol_index |
A broker's order symbol to broker token dictionary, for one mapping date. |
units_per_lot |
How many underlying units one lot of an instrument is. |
CacheCandidateSource
¶
Candidates from the three tier mapping cache, for the current mapping date.
Attributes:
| Name | Type | Description |
|---|---|---|
mapping_cache |
MappingCache
|
The process's mapping cache. |
Wrap a mapping cache.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
mapping_cache
|
MappingCache
|
The process's mapping cache, shared with anything else in the process that resolves instruments. |
required |
Returns:
| Type | Description |
|---|---|
None
|
This function returns nothing. |
Example programs
"""Asks the mapping cache, through `CacheCandidateSource`, which instruments a broker's tokens can be and what every broker calls them.
`TickResolver` never searches the mappings itself. It asks a candidate source three questions: which mapping date answers for a trading day, every identity a list of broker tokens maps to inside some segments, and every broker's order handle for some instruments. `CacheCandidateSource` answers them from the process's three-tier `MappingCache`, which in the live service is backed by Redis and TimescaleDB.
This program gives it a small stand-in cache holding two Zerodha tokens for 2026-09-15, so it runs without any data store. The stand-in answers the same three calls the real `MappingCache` does, with the same shapes, and refuses a day before its mapping date by answering None, which is how the real cache tells the resolver to look elsewhere. Notice that the segment filter matters: token 738561 is searched only among the segments the caller names.
Run it from the project root:
python examples/stock_brokers/instruments/ticks/utilities/resolution/CacheCandidateSource/example_1_asking_the_mapping_cache.py
"""
import datetime
from stock_brokers.instruments.ticks.utilities.resolution import (
CacheCandidateSource,
)
class StandInMappingCache:
"""A stand-in for `MappingCache` that holds a few identities and handles in memory.
Attributes:
mapping_date (datetime.date): The one mapping date this cache holds.
identities (dict): (broker, token) to the list of identities the token maps to.
handles (dict): Instrument id to broker name to that broker's order handle.
engine (None): The real cache's database engine, which this program never uses.
"""
def __init__(self):
"""Fills the cache with RELIANCE on NSE and a crude oil future on MCX.
Returns:
None: This method returns nothing.
"""
self.mapping_date = datetime.date(2026, 9, 15)
self.engine = None
reliance = {
'instrument_id': '11111111-1111-5111-8111-000000000102',
'exchange': 'nse',
'segment': 'nse_equities',
'shape': 'security',
'symbol': 'RELIANCE',
'underlying_symbol': None,
'expiry_date': None,
'strike_price': None,
'option_type': None,
}
crude_oil = {
'instrument_id': '11111111-1111-5111-8111-000000000101',
'exchange': 'mcx',
'segment': 'mcx_commodity_futures',
'shape': 'future',
'symbol': 'CRUDEOIL26OCTFUT',
'underlying_symbol': 'CRUDEOIL',
'expiry_date': datetime.date(2026, 10, 19),
'strike_price': None,
'option_type': None,
}
self.identities = {
('zerodha', '738561'): [
reliance,
],
('zerodha', '5720583'): [
crude_oil,
],
}
self.handles = {
reliance['instrument_id']: {
'zerodha': {
'broker_token': '738561',
'order_symbol': 'RELIANCE',
'lot_size': '1',
'tick_size': '0.1',
},
'dhan': {
'broker_token': '2885',
'order_symbol': 'RELIANCE',
'lot_size': '1',
'tick_size': '0.1',
},
},
}
def resolves_to_current_date(self, as_of_date):
"""Answers the mapping date for a day, or None for a day before it.
Args:
as_of_date (datetime.date): The trading day asked about.
Returns:
datetime.date | None: The mapping date, or None.
"""
if as_of_date < self.mapping_date:
return None
return self.mapping_date
def candidates_for_tokens(self, broker, broker_tokens, as_of_date, segments=None):
"""Answers every identity each token maps to inside the segments.
Args:
broker (str): The broker name.
broker_tokens (list): The broker's tokens.
as_of_date (datetime.date): The trading day.
segments (list | None): The segments to search, or None for any.
Returns:
dict: Token to a list of identities; tokens with none are absent.
"""
found = {}
for token in broker_tokens:
matches = []
for identity in self.identities.get((broker, str(token)), []):
if segments is None or identity['segment'] in segments:
matches.append(identity)
if matches:
found[str(token)] = matches
return found
def order_handles_for_instruments(self, instrument_identifiers, as_of_date, brokers=None):
"""Answers every broker's order handle for each instrument.
Args:
instrument_identifiers (list): The instrument ids.
as_of_date (datetime.date): The trading day.
brokers (list | None): Brokers to restrict the answer to, or None for all.
Returns:
dict: Instrument id to broker name to handle; instruments with none are absent.
"""
found = {}
for instrument_id in instrument_identifiers:
if instrument_id in self.handles:
found[instrument_id] = self.handles[instrument_id]
return found
class AskingTheMappingCacheExample:
"""Asks a candidate source the resolver's three questions and prints the answers.
Attributes:
source (CacheCandidateSource): The source being shown.
"""
def __init__(self):
"""Wraps the stand-in mapping cache in a candidate source.
Returns:
None: This method returns nothing.
"""
self.source = CacheCandidateSource(StandInMappingCache())
def run(self):
"""Prints the mapping date, candidates and handles.
Returns:
None: This method returns nothing.
"""
today = datetime.date(2026, 9, 15)
last_week = datetime.date(2026, 9, 8)
print(f'Mapping date for {today}: {self.source.mapping_date(today)}')
print(f'Mapping date for {last_week}: {self.source.mapping_date(last_week)}')
tokens = [
'738561',
'5720583',
'999',
]
cash_segments = (
'nse_equities',
'nse_exchange_traded_funds',
)
found = self.source.candidates('zerodha', tokens, today, cash_segments)
print(f'Tokens {tokens} among NSE cash segments:')
for token, identities in found.items():
for identity in identities:
print(f" {token} -> {identity['symbol']} ({identity['segment']}) {identity['instrument_id']}")
commodity_segments = (
'mcx_commodity_futures',
)
found = self.source.candidates('zerodha', tokens, today, commodity_segments)
print(f'The same tokens among MCX commodity futures: {sorted(found)}')
instrument_ids = [
'11111111-1111-5111-8111-000000000102',
]
handles = self.source.handles(instrument_ids, today)
for instrument_id, by_broker in handles.items():
for broker_name, handle in by_broker.items():
print(f"Handle for {instrument_id} at {broker_name}: token {handle['broker_token']}, lot {handle['lot_size']}, tick {handle['tick_size']}")
if __name__ == '__main__':
AskingTheMappingCacheExample().run()
It prints:
Mapping date for 2026-09-15: 2026-09-15
Mapping date for 2026-09-08: None
Tokens ['738561', '5720583', '999'] among NSE cash segments:
738561 -> RELIANCE (nse_equities) 11111111-1111-5111-8111-000000000102
The same tokens among MCX commodity futures: ['5720583']
Handle for 11111111-1111-5111-8111-000000000102 at zerodha: token 738561, lot 1, tick 0.1
Handle for 11111111-1111-5111-8111-000000000102 at dhan: token 2885, lot 1, tick 0.1
"""Reads Fyers' order symbol to token dictionary through `CacheCandidateSource.order_symbols`.
Most brokers' ticks carry a token, but a Fyers tick names its instrument by symbol, such as `NSE:SBIN-EQ`. The broker mappings store that symbol as the order symbol, so the resolver asks the candidate source once per mapping date for the whole symbol-to-token dictionary, which is one query against `unified.broker_mappings`. For a day the cache cannot answer, the source returns an empty dictionary without querying at all.
The query goes through the mapping cache's SQLAlchemy engine. This program gives the source a stand-in cache whose `engine` answers that one query with three recorded-looking rows and records the SQL and parameters it was sent, so the output shows exactly what would have been asked of TimescaleDB. When two rows share a symbol, the first token is kept. No database is reached.
Run it from the project root:
python examples/stock_brokers/instruments/ticks/utilities/resolution/CacheCandidateSource/example_2_order_symbols_for_fyers.py
"""
import datetime
from stock_brokers.instruments.ticks.utilities.resolution import (
CacheCandidateSource,
)
class StandInConnection:
"""A stand-in for a SQLAlchemy connection that answers one query with fixed rows.
Attributes:
engine (StandInEngine): The engine that opened this connection, which records what was executed.
"""
def __init__(self, engine):
"""Remembers the engine.
Args:
engine (StandInEngine): The engine that opened this connection.
Returns:
None: This method returns nothing.
"""
self.engine = engine
def __enter__(self):
"""Opens the connection.
Returns:
StandInConnection: This connection.
"""
return self
def __exit__(self, error_type, error, traceback):
"""Closes the connection.
Args:
error_type (type | None): The exception type, if one was raised.
error (BaseException | None): The exception, if one was raised.
traceback (object | None): Its traceback.
Returns:
bool: False, so an exception is not swallowed.
"""
return False
def execute(self, statement, parameters):
"""Records the statement and returns Fyers' rows.
Args:
statement (sqlalchemy.TextClause): The SQL.
parameters (dict): The bound parameters.
Returns:
list: Rows of (order_symbol, broker_token).
"""
self.engine.executed.append((str(statement), parameters))
return [
(
'NSE:SBIN-EQ',
'3045',
),
(
'NSE:RELIANCE-EQ',
'2885',
),
(
'NSE:SBIN-EQ',
'99999',
),
]
class StandInEngine:
"""A stand-in for a SQLAlchemy engine that records every query.
Attributes:
executed (list): (SQL, parameters) pairs, in order.
"""
def __init__(self):
"""Starts with nothing executed.
Returns:
None: This method returns nothing.
"""
self.executed = []
def connect(self):
"""Opens a stand-in connection.
Returns:
StandInConnection: The connection.
"""
return StandInConnection(self)
class StandInMappingCache:
"""A stand-in for `MappingCache` that knows its mapping date and holds an engine.
Attributes:
engine (StandInEngine): The stand-in engine.
mapping_date (datetime.date): The mapping date the cache holds.
"""
def __init__(self):
"""Builds the cache for 2026-09-15.
Returns:
None: This method returns nothing.
"""
self.engine = StandInEngine()
self.mapping_date = datetime.date(2026, 9, 15)
def resolves_to_current_date(self, as_of_date):
"""Answers the mapping date for a day, or None for a day before it.
Args:
as_of_date (datetime.date): The trading day asked about.
Returns:
datetime.date | None: The mapping date, or None.
"""
if as_of_date < self.mapping_date:
return None
return self.mapping_date
class OrderSymbolsForFyersExample:
"""Reads Fyers' order symbols for today and for a day the cache cannot answer.
Attributes:
cache (StandInMappingCache): The stand-in mapping cache.
source (CacheCandidateSource): The source being shown.
"""
def __init__(self):
"""Builds the source over the stand-in cache.
Returns:
None: This method returns nothing.
"""
self.cache = StandInMappingCache()
self.source = CacheCandidateSource(self.cache)
def run(self):
"""Prints the dictionaries and the query that was sent.
Returns:
None: This method returns nothing.
"""
symbols = self.source.order_symbols('fyers', datetime.date(2026, 9, 15))
print(f'Order symbols on 2026-09-15: {symbols}')
earlier = self.source.order_symbols('fyers', datetime.date(2026, 9, 1))
print(f'Order symbols on 2026-09-01: {earlier}')
print(f'Queries sent: {len(self.cache.engine.executed)}')
for sql, parameters in self.cache.engine.executed:
print(f' {sql}')
print(f' {parameters}')
if __name__ == '__main__':
OrderSymbolsForFyersExample().run()
It prints:
Order symbols on 2026-09-15: {'NSE:SBIN-EQ': '3045', 'NSE:RELIANCE-EQ': '2885'}
Order symbols on 2026-09-01: {}
Queries sent: 1
SELECT order_symbol, broker_token FROM unified.broker_mappings WHERE broker = :broker AND mapping_date = :mapping_date AND order_symbol IS NOT NULL
{'broker': 'fyers', 'mapping_date': datetime.date(2026, 9, 15)}
Methods:
| Name | Description |
|---|---|
order_symbols |
A broker's order symbol to token dictionary for the current mapping date. |
mapping_date |
The mapping date that answers for a day. |
candidates |
Every identity each token maps to inside the segments. |
handles |
Every broker's order handle - token, symbol, lot size, tick size - for each instrument. |
Source code in stock_brokers/instruments/ticks/utilities/resolution.py
order_symbols
¶
A broker's order symbol to token dictionary for the current mapping date.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
broker
|
str
|
The broker. |
required |
as_of_date
|
date
|
The India trading day. |
required |
Returns:
| Type | Description |
|---|---|
dict
|
Order symbol to broker token. |
Example usage
symbols = self.source.order_symbols('fyers', datetime.date(2026, 9, 15))
earlier = self.source.order_symbols('fyers', datetime.date(2026, 9, 1))
Source code in stock_brokers/instruments/ticks/utilities/resolution.py
mapping_date
¶
The mapping date that answers for a day.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
as_of_date
|
date
|
The India trading day the ticks belong to. |
required |
Returns:
| Type | Description |
|---|---|
date | None
|
The current mapping date, or None when the cache cannot answer for that day. |
Example usage
print(f'Mapping date for {today}: {self.source.mapping_date(today)}')
print(f'Mapping date for {last_week}: {self.source.mapping_date(last_week)}')
Source code in stock_brokers/instruments/ticks/utilities/resolution.py
candidates
¶
Every identity each token maps to inside the segments.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
broker
|
str
|
The broker name. |
required |
broker_tokens
|
list[str]
|
Tokens as stored in unified.broker_mappings. |
required |
as_of_date
|
date
|
The India trading day. |
required |
segments
|
tuple[str]
|
Canonical segments to search. |
required |
Returns:
| Type | Description |
|---|---|
dict
|
Token to a list of identity dicts. |
Example usage
found = self.source.candidates('zerodha', tokens, today, cash_segments)
found = self.source.candidates('zerodha', tokens, today, commodity_segments)
Source code in stock_brokers/instruments/ticks/utilities/resolution.py
handles
¶
Every broker's order handle - token, symbol, lot size, tick size - for each instrument.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument_ids
|
list[str]
|
Instrument ids. |
required |
as_of_date
|
date
|
The India trading day. |
required |
Returns:
| Type | Description |
|---|---|
dict
|
Instrument id to broker name to handle dict. |
Example usage
Source code in stock_brokers/instruments/ticks/utilities/resolution.py
TickResolver
¶
Instrument plans for broker tokens, compiled once per token per mapping date.
Attributes:
| Name | Type | Description |
|---|---|---|
unresolved |
Counter
|
(broker, token, reason) to ticks that could not be resolved since last drained, where reason is one of no_normalizer, unplaceable, unmapped, ambiguous or no_mapping_date. |
notes |
dict
|
(broker, token) to a note about a compiled plan, such as a lot size disagreement. |
as_of_date |
date | None
|
The India trading day plans are compiled for. |
mapping_date |
date | None
|
The mapping date plans are compiled from. |
Build the resolver.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
source
|
CacheCandidateSource
|
Where candidates come from. |
required |
normalizers
|
dict
|
Broker name to TickNormalizer. |
required |
logger
|
Logger | None
|
Where decisions worth knowing are reported. |
None
|
Returns:
| Type | Description |
|---|---|
None
|
This function returns nothing. |
Example programs
"""Compiles instrument plans for Zerodha's subscribed tokens before their first ticks arrive, and reads them back.
`TickResolver` turns the token on a tick into an `InstrumentPlan`. The live service calls `prewarm` with the broker's whole subscription set when it starts, so the mapping search happens once, off the per-tick path; after that `plan_for` is a single dictionary look-up. Plans are filed under the tick's spelling, which for Kite is an integer, while the subscription set holds strings.
Compiling a plan decides the instrument's lot size from every broker's order handle. On MCX the authority is Groww's lot, the only one that makes price times quantity the contract's value; elsewhere the most common lot wins, and a disagreement is kept in `notes` and logged once. This program gives the resolver a stand-in mapping cache with three Zerodha instruments and a stand-in logger that prints, so it needs no data store. The brokers' disagreement over the NIFTY future's lot is invented for the demonstration. The clock is fixed at 09:30 India time on 2026-09-15.
The subscription set also holds `NSE:INFY`, which is not a Kite token, so it is recorded as unplaceable. That miss is recorded before the resolver has first read its mapping date, so the first `refresh` counts it as a plan to drop and logs that plans will be recompiled, although none had been compiled yet.
Run it from the project root:
python examples/stock_brokers/instruments/ticks/utilities/resolution/TickResolver/example_1_prewarming_zerodha_plans.py
"""
import datetime
import zoneinfo
from stock_brokers.instruments.ticks.utilities.registry import (
build_normalizers,
)
from stock_brokers.instruments.ticks.utilities.resolution import (
CacheCandidateSource,
TickResolver,
)
INDIA = zoneinfo.ZoneInfo('Asia/Kolkata')
class PrintingLogger:
"""A stand-in logger that prints each message with its level."""
def info(self, message):
"""Prints an informational message.
Args:
message (str): The message.
Returns:
None: This method returns nothing.
"""
print(f'LOG info: {message}')
def warning(self, message):
"""Prints a warning.
Args:
message (str): The message.
Returns:
None: This method returns nothing.
"""
print(f'LOG warning: {message}')
class StandInMappingCache:
"""A stand-in for `MappingCache` holding three Zerodha instruments for 2026-09-15.
Attributes:
mapping_date (datetime.date): The one mapping date this cache holds.
identities (dict): Zerodha token to the identities it maps to.
handles (dict): Instrument id to broker name to order handle.
engine (None): The real cache's database engine, which this program never uses.
"""
def __init__(self):
"""Fills the cache.
Returns:
None: This method returns nothing.
"""
self.mapping_date = datetime.date(2026, 9, 15)
self.engine = None
self.identities = {
'738561': [
self.identity('102', 'nse', 'nse_equities', 'security', 'RELIANCE', None, None),
],
'5720583': [
self.identity('101', 'mcx', 'mcx_commodity_futures', 'future', 'CRUDEOIL26OCTFUT', 'CRUDEOIL', '2026-10-19'),
],
'13238786': [
self.identity('105', 'nse', 'nse_equity_index_futures', 'future', 'NIFTY26SEPFUT', 'NIFTY', '2026-09-29'),
],
}
self.handles = {
self.instrument_id('102'): {
'zerodha': self.handle('738561', '1'),
'dhan': self.handle('2885', '1'),
},
self.instrument_id('101'): {
'zerodha': self.handle('5720583', '100'),
'kotak': self.handle('440001', '1'),
'groww': self.handle('440001', '100'),
},
self.instrument_id('105'): {
'zerodha': self.handle('13238786', '75'),
'dhan': self.handle('35003', '75'),
'kotak': self.handle('35003', '65'),
},
}
def instrument_id(self, suffix):
"""Builds an instrument id.
Args:
suffix (str): The last digits of the id.
Returns:
str: The id.
"""
return f'11111111-1111-5111-8111-000000000{suffix}'
def identity(self, suffix, exchange, segment, shape, symbol, underlying_symbol, expiry_date):
"""Builds one identity as the real cache returns it.
Args:
suffix (str): The last digits of the instrument id.
exchange (str): The canonical exchange.
segment (str): The canonical segment.
shape (str): "security", "future" or "option".
symbol (str): The trading symbol.
underlying_symbol (str | None): The underlying, for a derivative.
expiry_date (str | None): The expiry, for a derivative.
Returns:
dict: The identity.
"""
return {
'instrument_id': self.instrument_id(suffix),
'exchange': exchange,
'segment': segment,
'shape': shape,
'symbol': symbol,
'underlying_symbol': underlying_symbol,
'expiry_date': expiry_date,
'strike_price': None,
'option_type': None,
}
def handle(self, broker_token, lot_size):
"""Builds one broker's order handle.
Args:
broker_token (str): The broker's token.
lot_size (str): The broker's lot size.
Returns:
dict: The handle.
"""
return {
'broker_token': broker_token,
'order_symbol': None,
'lot_size': lot_size,
'tick_size': '0.05',
}
def resolves_to_current_date(self, as_of_date):
"""Answers the mapping date for a day, or None for a day before it.
Args:
as_of_date (datetime.date): The trading day asked about.
Returns:
datetime.date | None: The mapping date, or None.
"""
if as_of_date < self.mapping_date:
return None
return self.mapping_date
def candidates_for_tokens(self, broker, broker_tokens, as_of_date, segments=None):
"""Answers every identity each token maps to inside the segments.
Args:
broker (str): The broker name.
broker_tokens (list): The broker's tokens.
as_of_date (datetime.date): The trading day.
segments (list | None): The segments to search, or None for any.
Returns:
dict: Token to a list of identities; tokens with none are absent.
"""
found = {}
for token in broker_tokens:
matches = []
for identity in self.identities.get(token, []):
if segments is None or identity['segment'] in segments:
matches.append(identity)
if matches:
found[token] = matches
return found
def order_handles_for_instruments(self, instrument_identifiers, as_of_date, brokers=None):
"""Answers every broker's order handle for each instrument.
Args:
instrument_identifiers (list): The instrument ids.
as_of_date (datetime.date): The trading day.
brokers (list | None): Brokers to restrict the answer to, or None for all.
Returns:
dict: Instrument id to broker name to handle.
"""
found = {}
for instrument_id in instrument_identifiers:
if instrument_id in self.handles:
found[instrument_id] = self.handles[instrument_id]
return found
class PrewarmingZerodhaPlansExample:
"""Prewarms Zerodha's subscription set and prints the compiled plans.
Attributes:
resolver (TickResolver): The resolver being shown.
now (float): The fixed current instant, in epoch seconds.
"""
def __init__(self):
"""Builds the resolver over the stand-in cache with Zerodha's normalizer.
Returns:
None: This method returns nothing.
"""
source = CacheCandidateSource(StandInMappingCache())
brokers = [
'zerodha',
]
self.resolver = TickResolver(source, build_normalizers(brokers), PrintingLogger())
moment = datetime.datetime(2026, 9, 15, 9, 30, tzinfo=INDIA)
self.now = moment.timestamp()
def run(self):
"""Prewarms, looks one plan up, and prints every plan and note.
Returns:
None: This method returns nothing.
"""
subscriptions = [
'738561',
'5720583',
'13238786',
'NSE:INFY',
]
planned, total = self.resolver.prewarm('zerodha', subscriptions, self.now)
print(f'Prewarmed {planned} of {total} subscribed tokens')
print(f'As of {self.resolver.as_of_date}, from mapping date {self.resolver.mapping_date}')
plan = self.resolver.plan_for('zerodha', 738561, self.now)
print(f'plan_for 738561: {plan.instrument_id} {plan.segment}, session closes trading at {plan.session.trading_close} seconds')
for key, compiled in self.resolver.plans().items():
broker_name, token = key
print(f"{broker_name} {token!r}: {compiled.shape} on {compiled.exchange}, lot {compiled.lot_size}, volume multiplier {compiled.multipliers['volume']}, close policy {compiled.close_policy}")
print(f'Notes: {self.resolver.notes}')
print(f'Unresolved: {dict(self.resolver.drain_unresolved())}')
if __name__ == '__main__':
PrewarmingZerodhaPlansExample().run()
It prints:
LOG warning: zerodha token NSE:INFY did not resolve: unplaceable.
LOG info: Mapping date is now 2026-09-15; instrument plans will be recompiled.
LOG warning: zerodha 13238786 (nse_equity_index_futures): lot size 75 - brokers disagree: 75x2, 65x1.
Prewarmed 3 of 4 subscribed tokens
As of 2026-09-15, from mapping date 2026-09-15
plan_for 738561: 11111111-1111-5111-8111-000000000102 nse_equities, session closes trading at 55800 seconds
zerodha 738561: security on nse, lot 1, volume multiplier 1, close policy always
zerodha 5720583: future on mcx, lot 100, volume multiplier 100, close policy always
zerodha 13238786: future on nse, lot 75, volume multiplier 1, close policy always
Notes: {('zerodha', 13238786): 'brokers disagree: 75x2, 65x1'}
Unresolved: {('zerodha', 'NSE:INFY', 'unplaceable'): 1}
"""Shows how `TickResolver` refuses to guess, how long it leaves a miss alone, and what a new mapping date does to its plans.
A token that does not resolve to exactly one live instrument gets no plan. The resolver records why: `unplaceable` when the broker's normalizer cannot read the token, `unmapped` when nothing in the mappings matches it (an expired contract counts as no match), `ambiguous` when it matches more than one instrument, and `no_normalizer` for a broker it has no normalizer for. Writing one instrument's prices under another's id would be worse than writing nothing, so an ambiguous token is never resolved to its first candidate.
A miss is remembered for ten minutes, so a token nothing maps costs one search per ten minutes rather than one per tick. `drain_unresolved` hands the counts to whoever reports them and starts again. `refresh` is called every thirty seconds by the live service; when the mapping date moves, after the morning's download, it drops every plan so they are compiled again from the new mappings.
The stand-in mapping cache counts every search it answers, which shows when the resolver asks again. A stand-in logger prints each warning, and the clock is passed in, fixed at 09:30 India time on 2026-09-15. No data store is involved.
Run it from the project root:
python examples/stock_brokers/instruments/ticks/utilities/resolution/TickResolver/example_2_misses_retries_and_a_new_mapping_date.py
"""
import datetime
import zoneinfo
from stock_brokers.instruments.ticks.utilities.registry import (
build_normalizers,
)
from stock_brokers.instruments.ticks.utilities.resolution import (
CacheCandidateSource,
TickResolver,
)
INDIA = zoneinfo.ZoneInfo('Asia/Kolkata')
class PrintingLogger:
"""A stand-in logger that prints each message with its level."""
def info(self, message):
"""Prints an informational message.
Args:
message (str): The message.
Returns:
None: This method returns nothing.
"""
print(f'LOG info: {message}')
def warning(self, message):
"""Prints a warning.
Args:
message (str): The message.
Returns:
None: This method returns nothing.
"""
print(f'LOG warning: {message}')
class CountingMappingCache:
"""A stand-in for `MappingCache` that counts the searches it answers.
Attributes:
mapping_date (datetime.date): The mapping date the cache holds, which the program moves.
searches (int): How many candidate searches have been answered.
identities (dict): Zerodha token to the identities it maps to.
engine (None): The real cache's database engine, which this program never uses.
"""
def __init__(self):
"""Fills the cache with one good token, one ambiguous token and one expired contract.
Returns:
None: This method returns nothing.
"""
self.mapping_date = datetime.date(2026, 9, 15)
self.searches = 0
self.engine = None
self.identities = {
'738561': [
self.identity('102', 'RELIANCE', None),
],
'408065': [
self.identity('107', 'INFY', None),
self.identity('108', 'INFY-BE', None),
],
'13238786': [
self.identity('109', 'NIFTY26AUGFUT', '2026-08-27'),
],
}
def identity(self, suffix, symbol, expiry_date):
"""Builds one identity as the real cache returns it.
Args:
suffix (str): The last digits of the instrument id.
symbol (str): The trading symbol.
expiry_date (str | None): The expiry, for a derivative.
Returns:
dict: The identity.
"""
segment = 'nse_equities'
shape = 'security'
if expiry_date is not None:
segment = 'nse_equity_index_futures'
shape = 'future'
return {
'instrument_id': f'11111111-1111-5111-8111-000000000{suffix}',
'exchange': 'nse',
'segment': segment,
'shape': shape,
'symbol': symbol,
'underlying_symbol': None,
'expiry_date': expiry_date,
'strike_price': None,
'option_type': None,
}
def resolves_to_current_date(self, as_of_date):
"""Answers the mapping date for a day, or None for a day before it.
Args:
as_of_date (datetime.date): The trading day asked about.
Returns:
datetime.date | None: The mapping date, or None.
"""
if as_of_date < self.mapping_date:
return None
return self.mapping_date
def candidates_for_tokens(self, broker, broker_tokens, as_of_date, segments=None):
"""Answers every identity each token maps to inside the segments, and counts the search.
Args:
broker (str): The broker name.
broker_tokens (list): The broker's tokens.
as_of_date (datetime.date): The trading day.
segments (list | None): The segments to search, or None for any.
Returns:
dict: Token to a list of identities; tokens with none are absent.
"""
self.searches += 1
found = {}
for token in broker_tokens:
matches = []
for identity in self.identities.get(token, []):
if segments is None or identity['segment'] in segments:
matches.append(identity)
if matches:
found[token] = matches
return found
def order_handles_for_instruments(self, instrument_identifiers, as_of_date, brokers=None):
"""Answers each instrument's handles, which here are only Zerodha's lot of 1.
Args:
instrument_identifiers (list): The instrument ids.
as_of_date (datetime.date): The trading day.
brokers (list | None): Brokers to restrict the answer to, or None for all.
Returns:
dict: Instrument id to broker name to handle.
"""
found = {}
for instrument_id in instrument_identifiers:
found[instrument_id] = {
'zerodha': {
'broker_token': None,
'order_symbol': None,
'lot_size': '1',
'tick_size': '0.05',
},
}
return found
class MissesAndRetriesExample:
"""Resolves tokens that miss, retries them, and moves the mapping date.
Attributes:
cache (CountingMappingCache): The stand-in mapping cache.
resolver (TickResolver): The resolver being shown.
now (float): The fixed current instant, in epoch seconds.
"""
def __init__(self):
"""Builds the resolver with Zerodha's normalizer only.
Returns:
None: This method returns nothing.
"""
self.cache = CountingMappingCache()
brokers = [
'zerodha',
]
self.resolver = TickResolver(CacheCandidateSource(self.cache), build_normalizers(brokers), PrintingLogger())
moment = datetime.datetime(2026, 9, 15, 9, 30, tzinfo=INDIA)
self.now = moment.timestamp()
def run(self):
"""Prints each step's outcome.
Returns:
None: This method returns nothing.
"""
self.resolver.refresh(self.now)
tokens = [
738561,
408065,
13238786,
1025,
'NSE:INFY',
]
compiled = self.resolver.compile('zerodha', tokens, self.now)
print(f'Compiled: {sorted(compiled)}')
print(f'Searches so far: {self.cache.searches}')
print(f"plan_for upstox 42: {self.resolver.plan_for('upstox', 42, self.now)}")
five_minutes_later = self.now + 300
print(f"plan_for 408065 five minutes later: {self.resolver.plan_for('zerodha', 408065, five_minutes_later)}, searches {self.cache.searches}")
eleven_minutes_later = self.now + 660
print(f"plan_for 408065 eleven minutes later: {self.resolver.plan_for('zerodha', 408065, eleven_minutes_later)}, searches {self.cache.searches}")
drained = self.resolver.drain_unresolved()
for key in sorted(drained, key=str):
print(f'Unresolved {key}: {drained[key]}')
print(f'Unresolved after draining: {dict(self.resolver.unresolved)}')
print(f'refresh later the same day: {self.resolver.refresh(self.now + 3600)}')
self.cache.mapping_date = datetime.date(2026, 9, 16)
next_morning = datetime.datetime(2026, 9, 16, 8, 0, tzinfo=INDIA)
print(f'refresh after the next mapping date: {self.resolver.refresh(next_morning.timestamp())}')
print(f'Plans left: {self.resolver.plans()}, mapping date {self.resolver.mapping_date}')
if __name__ == '__main__':
MissesAndRetriesExample().run()
It prints:
LOG warning: zerodha token NSE:INFY did not resolve: unplaceable.
LOG warning: zerodha token 408065 did not resolve: ambiguous (nse_equities:INFY, nse_equities:INFY-BE).
LOG warning: zerodha token 1025 did not resolve: unmapped.
LOG warning: zerodha token 13238786 did not resolve: unmapped.
Compiled: [('zerodha', 738561)]
Searches so far: 2
LOG warning: upstox token 42 did not resolve: no_normalizer.
plan_for upstox 42: None
plan_for 408065 five minutes later: None, searches 2
plan_for 408065 eleven minutes later: None, searches 3
Unresolved ('upstox', '42', 'no_normalizer'): 1
Unresolved ('zerodha', '1025', 'unmapped'): 1
Unresolved ('zerodha', '13238786', 'unmapped'): 1
Unresolved ('zerodha', '408065', 'ambiguous'): 2
Unresolved ('zerodha', 'NSE:INFY', 'unplaceable'): 1
Unresolved after draining: {}
refresh later the same day: False
LOG info: Mapping date is now 2026-09-16; instrument plans will be recompiled.
refresh after the next mapping date: True
Plans left: {}, mapping date 2026-09-16
Methods:
| Name | Description |
|---|---|
refresh |
Move to the India trading day an instant falls on, dropping every plan if the mapping changed. |
plan_for |
The plan for a tick's token, compiling it on first sight. |
prewarm |
Compile plans for every token a broker's feed is subscribed to, before its ticks arrive. |
compile |
Resolve a batch of one broker's tokens and compile their plans. |
drain_unresolved |
Take the unresolved counts accumulated since the last call. |
plans |
Every compiled plan, by (broker, token). |
Source code in stock_brokers/instruments/ticks/utilities/resolution.py
refresh
¶
Move to the India trading day an instant falls on, dropping every plan if the mapping changed.
The live service calls this every thirty seconds with the wall clock.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
epoch
|
float
|
An instant on the day to resolve for. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True when the plans were dropped because the day's mapping date differs from theirs. |
Example usage
print(f'refresh later the same day: {self.resolver.refresh(self.now + 3600)}')
print(f'refresh after the next mapping date: {self.resolver.refresh(next_morning.timestamp())}')
Source code in stock_brokers/instruments/ticks/utilities/resolution.py
plan_for
¶
The plan for a tick's token, compiling it on first sight.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
broker
|
str
|
The broker the tick came from. |
required |
instrument_token
|
int | str
|
The tick's |
required |
now
|
float
|
The current instant, used to decide when a miss may be retried. |
required |
Returns:
| Type | Description |
|---|---|
InstrumentPlan | None
|
The plan, or None when the token does not resolve to one instrument. |
Example usage
plan = self.resolver.plan_for('zerodha', 738561, self.now)
print(f"plan_for upstox 42: {self.resolver.plan_for('upstox', 42, self.now)}")
print(f"plan_for 408065 five minutes later: {self.resolver.plan_for('zerodha', 408065, five_minutes_later)}, searches {self.cache.searches}")
Source code in stock_brokers/instruments/ticks/utilities/resolution.py
prewarm
¶
Compile plans for every token a broker's feed is subscribed to, before its ticks arrive.
This is what keeps the mapping search off the per-tick path: by the time a subscribed instrument's first tick arrives its plan is already filed under the tick's spelling.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
broker
|
str
|
The broker name. |
required |
subscription_tokens
|
list[str]
|
Members of the broker's subscription set. |
required |
now
|
float
|
The current instant. |
required |
Returns:
| Type | Description |
|---|---|
tuple[int, int]
|
How many of the tokens have a plan, and how many there were. |
Example usage
planned, total = self.resolver.prewarm('zerodha', subscriptions, self.now)
Source code in stock_brokers/instruments/ticks/utilities/resolution.py
compile
¶
Resolve a batch of one broker's tokens and compile their plans.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
broker
|
str
|
The broker name. |
required |
instrument_tokens
|
list
|
Tokens spelled as they appear on ticks. |
required |
now
|
float
|
The current instant. |
required |
Returns:
| Type | Description |
|---|---|
dict
|
(broker, token) to InstrumentPlan, for the tokens that resolved. |
Example usage
compiled = self.resolver.compile('zerodha', tokens, self.now)
Source code in stock_brokers/instruments/ticks/utilities/resolution.py
drain_unresolved
¶
Take the unresolved counts accumulated since the last call.
Returns:
| Type | Description |
|---|---|
Counter
|
(broker, token, reason) to count. |
Example usage
print(f'Unresolved: {dict(self.resolver.drain_unresolved())}')
Source code in stock_brokers/instruments/ticks/utilities/resolution.py
plans
¶
Every compiled plan, by (broker, token).
Returns:
| Type | Description |
|---|---|
dict
|
The plans; misses are left out. |
Example usage
print(f'Plans left: {self.resolver.plans()}, mapping date {self.resolver.mapping_date}')
Source code in stock_brokers/instruments/ticks/utilities/resolution.py
_compile_plan
¶
Compile the plan for one resolved token from its instrument's identity and every broker's handle.
The lot size is decided from the handles, and a note about it, such as brokers disagreeing, is recorded in notes and logged once. Each quantity field then gets the multiplier that turns this broker's figure into units: the lot size for a field reported in lots, the lot size divided by the broker's own lot size for a field reported in broker lots, None when either lot size is unknown, and 1 otherwise.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
normalizer
|
TickNormalizer
|
The broker's normalizer, which says how it reports each quantity field and its close. |
required |
broker
|
str
|
The broker name. |
required |
token
|
int | str
|
The token as it is spelled on the broker's ticks. |
required |
broker_token
|
str
|
The token as stored in unified.broker_mappings. |
required |
identity
|
dict
|
The one instrument identity the token resolved to. |
required |
handles_by_broker
|
dict
|
Broker name to that broker's order handle for the instrument. |
required |
Returns:
| Type | Description |
|---|---|
InstrumentPlan
|
The compiled plan. |
Source code in stock_brokers/instruments/ticks/utilities/resolution.py
_miss
¶
Record that a token did not resolve, and when it may be tried again.
The retry instant is filed in place of a plan, so plan_for returns None without searching again until then. The miss is counted in unresolved and logged once per broker, token and reason.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
broker
|
str
|
The broker name. |
required |
token
|
int | str
|
The token as it is spelled on the broker's ticks. |
required |
reason
|
str
|
Why it did not resolve: no_normalizer, unplaceable, unmapped, ambiguous or no_mapping_date. |
required |
retry_at
|
float
|
The epoch instant before which the token is not searched for again. |
required |
detail
|
str | None
|
More about the miss for the log line, such as the candidates of an ambiguous token. |
None
|
Source code in stock_brokers/instruments/ticks/utilities/resolution.py
_log_once
¶
Log a warning the first time a key is seen since the plans were last dropped.
Nothing is logged when the resolver has no logger.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key
|
tuple
|
What identifies the message, so the same event is not logged twice. |
required |
message
|
str
|
The warning to log. |
required |
Source code in stock_brokers/instruments/ticks/utilities/resolution.py
order_symbol_index
¶
A broker's order symbol to broker token dictionary, for one mapping date.
The mapping cache is keyed by token, but a Fyers tick names its instrument by symbol, and the symbol is what the broker mappings store as the order symbol. One query per mapping date - about 160,000 rows for Fyers - builds the translation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
engine
|
Engine
|
An engine over TimescaleDB. |
required |
broker
|
str
|
The broker. |
required |
mapping_date
|
date
|
The mapping date. |
required |
Returns:
| Type | Description |
|---|---|
dict
|
Order symbol to broker token. A symbol listed twice keeps its first token. |
Source code in stock_brokers/instruments/ticks/utilities/resolution.py
_whole_number
¶
A lot size as a positive int, or None.
Source code in stock_brokers/instruments/ticks/utilities/resolution.py
units_per_lot
¶
How many underlying units one lot of an instrument is.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
identity
|
dict
|
The instrument's identity. |
required |
handles_by_broker
|
dict
|
Broker name to that broker's handle for the instrument. |
required |
Returns:
| Type | Description |
|---|---|
tuple[int | None, str | None]
|
The units per lot, or None when it cannot be decided, and a note when the brokers disagreed or the authority was missing. |
Source code in stock_brokers/instruments/ticks/utilities/resolution.py
_expired
¶
Whether a derivative had expired before a day, which rules it out as a live candidate.