dhan
¶
Dhan ticks.
Dhan identifies an instrument by an exchange segment and a security id. Its market feed publishes
the segment as the numeric code from the packet header - 5:565899 - while its subscription set
spells it by name - MCX_COMM:565899 - so both spellings are accepted. The security id is stored
bare in unified.broker_mappings and is reused across segments (2885 is RELIANCE on NSE and a
currency option too), which is why the segment has to come along.
What Dhan's values mean, as measured against the stored ticks of 2026-09-11 to 2026-09-13:
closeis the previous session's close while the session runs - CRUDEOIL SEP read 9722.0 at 22:46 on 2026-09-11, the same as Zerodha - and is the day's own close after it: RELIANCE read 1257.5 from the evening of 2026-09-12 onwards, where Zerodha's previous close read 1274.0. So it is used only before the session's close, and the previous close already seen carries through the evening. An NSE in-session reading has not been observed yet and is on the live checklist.- On MCX every quantity is in lots, identical to Zerodha's at the same moment (volume 67958, open
interest 18095).
oi_day_highandoi_day_lowarrive as 0 and are dropped. - Prices arrive as float32 and carry noise past the second place; rounding in the base class removes it.
- The feed sends no exchange timestamp, and its last trade time is corrected to a true epoch in the market feed.
Classes:
| Name | Description |
|---|---|
DhanTickNormalizer |
Normalizes Dhan ticks. |
DhanTickNormalizer
¶
Bases: TickNormalizer
Normalizes Dhan ticks.
Build the lookup from each Dhan numeric segment code to the canonical segments it can hold.
The table is worked out once here from SEGMENT_CODES, so feed_key only has to look a tick's numeric segment code up rather than recompute its segments on every call.
Example programs
"""Places Dhan feed tokens, which name the exchange segment by number, on the canonical segments they can be in.
Dhan names an instrument by an exchange segment and a security id. Its market feed spells the segment as the numeric code from the packet header, `5:565899`, while its subscription set spells it by name, `MCX_COMM:565899`, and the normalizer accepts both. The security id alone is not enough, because Dhan reuses it across segments: 2885 is RELIANCE on NSE cash (code 1) and something else among NSE derivatives (code 2), so the key always carries the segments to search. Code 6 is not a Dhan segment and a token without a security id is malformed, so both print `None`.
The normalizer only works on the text of each token, so the program needs no Redis, database, network or stand-in of any kind. Each output line shows a token as the feed spells it, then the broker token that `unified.broker_mappings` is searched with, the order symbol when the broker names instruments by symbol, and how many canonical segments the search is limited to, with the first of them. A token the normalizer cannot place prints `None`. `tick_spelling` does the reverse translation for the subscription set: it turns a segment name into the number ticks carry, keeps a numeric spelling as it is, and refuses a name it does not know.
Run it from the project root:
python examples/stock_brokers/instruments/ticks/dhan/DhanTickNormalizer/example_1_placing_feed_tokens.py
"""
from stock_brokers.instruments.ticks.dhan import (
DhanTickNormalizer,
)
class PlacingFeedTokensExample:
"""Asks the Dhan normalizer where each of a few feed tokens belongs.
Attributes:
normalizer (DhanTickNormalizer): The normalizer being shown.
tokens (list): The tokens to place, spelled as the feed spells them.
subscription_tokens (list): Tokens as the broker's subscription set stores them.
"""
def __init__(self):
"""Builds the normalizer and the tokens to place.
Returns:
None: This method returns nothing.
"""
self.normalizer = DhanTickNormalizer()
self.tokens = [
'5:565899',
'MCX_COMM:565899',
'1:2885',
'2:2885',
'0:13',
'6:100',
'NSE_EQ',
]
self.subscription_tokens = [
'MCX_COMM:565899',
'NSE_EQ:2885',
'5:565899',
'XYZ:1',
]
def describe(self, feed_key):
"""Describes a feed key in one line.
Args:
feed_key (FeedKey | None): The key the normalizer returned.
Returns:
str: The description.
"""
if feed_key is None:
return 'None'
segment_count = len(feed_key.segments)
first_segment = feed_key.segments[0]
return f'broker_token={feed_key.broker_token!r} order_symbol={feed_key.order_symbol!r} segments={segment_count} starting with {first_segment}'
def run(self):
"""Prints where each token belongs.
Returns:
None: This method returns nothing.
"""
print(f'Broker: {DhanTickNormalizer.BROKER_NAME}')
for token in self.tokens:
feed_key = self.normalizer.feed_key(token)
print(f'{token!r} -> {self.describe(feed_key)}')
print('Subscription tokens as ticks spell them:')
for token in self.subscription_tokens:
spelling = self.normalizer.tick_spelling(token)
print(f'{token!r} -> {spelling!r}')
if __name__ == '__main__':
PlacingFeedTokensExample().run()
It prints:
Broker: dhan
'5:565899' -> broker_token='565899' order_symbol=None segments=16 starting with mcx_fixed_income_futures
'MCX_COMM:565899' -> broker_token='565899' order_symbol=None segments=16 starting with mcx_fixed_income_futures
'1:2885' -> broker_token='2885' order_symbol=None segments=6 starting with nse_equities
'2:2885' -> broker_token='2885' order_symbol=None segments=16 starting with nse_fixed_income_futures
'0:13' -> broker_token='13' order_symbol=None segments=8 starting with nse_equity_indices
'6:100' -> None
'NSE_EQ' -> None
Subscription tokens as ticks spell them:
'MCX_COMM:565899' -> '5:565899'
'NSE_EQ:2885' -> '1:2885'
'5:565899' -> '5:565899'
'XYZ:1' -> None
"""Normalizes one Dhan tick for an MCX crude oil future, rounding away float32 noise and dropping values Dhan does not really send.
Dhan sends prices as single-precision floats, so 9731.4 arrives as 9731.400390625; the plan rounds prices to two places. Its MCX quantities are lots, like Kite's. Dhan sends no exchange timestamp worth keeping, so `exchange_time` is None, and its `oi_day_high` and `oi_day_low` arrive as 0 and are dropped. Its `close` is the previous close only while the session runs, so `reported_close` is kept before the close and dropped after it.
The plan that `normalize` applies is normally compiled by `TickResolver` from the mapping cache. This program builds it by hand with the same rules, asking the normalizer how it reports each quantity field and when its close may be trusted, so it needs no database. The tick is shaped like the ones the broker's market feed publishes, with fixed times, so the output is the same on every run. The program normalizes the tick twice, once as received before the session's close and once as received after it, which only changes whether `reported_close` is kept. Order book levels are printed as (price, quantity, orders).
Run it from the project root:
python examples/stock_brokers/instruments/ticks/dhan/DhanTickNormalizer/example_2_float32_prices_and_close.py
"""
import datetime
import zoneinfo
from stock_brokers.instruments.ticks import base
from stock_brokers.instruments.ticks.base import (
InstrumentPlan,
)
from stock_brokers.instruments.ticks.dhan import (
DhanTickNormalizer,
)
from stock_brokers.instruments.ticks.utilities import sessions
INDIA = zoneinfo.ZoneInfo('Asia/Kolkata')
class CrudeOilTickExample:
"""Normalizes one Dhan tick through a hand-built plan and prints the result.
Attributes:
normalizer (DhanTickNormalizer): The normalizer being shown.
received_at (float): When the tick was received, in epoch seconds.
tick (dict): The tick as the market feed published it.
plan (InstrumentPlan): The plan for the tick's instrument.
"""
def __init__(self):
"""Builds the normalizer, the tick and its plan.
Returns:
None: This method returns nothing.
"""
self.normalizer = DhanTickNormalizer()
received = datetime.datetime(2026, 9, 15, 22, 46, tzinfo=INDIA)
self.received_at = received.timestamp()
self.tick = {
'instrument_token': '5:565899',
'last_price': 9731.400390625,
'last_quantity': 2,
'average_price': 9718.4501953125,
'volume': 67958,
'buy_quantity': 4210,
'sell_quantity': 3987,
'ohlc': {
'open': 9702.0,
'high': 9745.2998046875,
'low': 9688.099609375,
'close': 9722.0,
},
'oi': 18095,
'oi_day_high': 0,
'oi_day_low': 0,
'last_trade_time': self.received_at - 2.0,
'exchange_timestamp': self.received_at - 1.0,
'received_at': self.received_at,
'depth': {
'buy': [
{
'price': 9731.2998046875,
'quantity': 3,
'orders': 2,
},
{
'price': 9731.2001953125,
'quantity': 11,
'orders': 4,
},
{
'price': 0.0,
'quantity': 0,
'orders': 0,
},
{
'price': 0.0,
'quantity': 0,
'orders': 0,
},
{
'price': 0.0,
'quantity': 0,
'orders': 0,
},
],
'sell': [
{
'price': 9731.5,
'quantity': 5,
'orders': 2,
},
{
'price': 9731.599609375,
'quantity': 8,
'orders': 3,
},
{
'price': 0.0,
'quantity': 0,
'orders': 0,
},
{
'price': 0.0,
'quantity': 0,
'orders': 0,
},
{
'price': 0.0,
'quantity': 0,
'orders': 0,
},
],
},
}
self.plan = self.build_plan()
def build_plan(self):
"""Compiles the plan for the tick's instrument the way the resolver would.
Returns:
InstrumentPlan: The plan.
"""
exchange = 'mcx'
segment = 'mcx_commodity_futures'
lot_size = 100
broker_lot_size = 1
multipliers = {}
for field in base.QUANTITY_FIELDS:
basis = self.normalizer.quantity_basis(exchange, field)
if basis == base.BASIS_LOTS:
multipliers[field] = lot_size
elif basis == base.BASIS_BROKER_LOTS:
multipliers[field] = lot_size // broker_lot_size
else:
multipliers[field] = 1
identity = {
'exchange': 'mcx',
'segment': 'mcx_commodity_futures',
'shape': 'future',
'symbol': 'CRUDEOIL26OCTFUT',
'underlying_symbol': 'CRUDEOIL',
'expiry_date': '2026-10-19',
'strike_price': None,
'option_type': None,
}
instrument_id = '11111111-1111-5111-8111-000000000101'
broker_token = '565899'
return InstrumentPlan(
instrument_id=instrument_id,
broker=DhanTickNormalizer.BROKER_NAME,
broker_token=broker_token,
exchange=exchange,
segment=segment,
shape=identity['shape'],
session=sessions.session_for(segment),
lot_size=lot_size,
multipliers=multipliers,
price_decimals=2,
negative_prices=True,
close_policy=self.normalizer.close_policy(exchange),
has_open_interest=identity['shape'] != 'security',
trusts_last_trade_time=DhanTickNormalizer.TRUSTS_LAST_TRADE_TIME,
trusts_exchange_time=DhanTickNormalizer.TRUSTS_EXCHANGE_TIME,
identity_json=base.identity_json(
instrument_id,
DhanTickNormalizer.BROKER_NAME,
broker_token,
identity,
lot_size,
),
)
def run(self):
"""Prints the feed key, the plan's rules and the normalized values.
Returns:
None: This method returns nothing.
"""
token = self.tick['instrument_token']
feed_key = self.normalizer.feed_key(token)
print(f'Token {token!r}: broker_token={feed_key.broker_token!r} order_symbol={feed_key.order_symbol!r}')
volume_basis = self.normalizer.quantity_basis(self.plan.exchange, 'volume')
print(f'Volume on {self.plan.exchange} is reported in {volume_basis}, multiplier {self.plan.multipliers["volume"]}')
print(f'Close policy on {self.plan.exchange}: {self.normalizer.close_policy(self.plan.exchange)}')
before_close = self.normalizer.normalize(self.tick, self.plan, True)
print('Normalized, received before the close:')
for name, value in before_close.items():
print(f' {name}: {value}')
after_close = self.normalizer.normalize(self.tick, self.plan, False)
print(f'reported_close when received after the close: {after_close["reported_close"]}')
if __name__ == '__main__':
CrudeOilTickExample().run()
It prints:
Token '5:565899': broker_token='565899' order_symbol=None
Volume on mcx is reported in lots, multiplier 100
Close policy on mcx: before
Normalized, received before the close:
last_price: 9731.4
last_quantity: 200
average_price: 9718.45
volume: 6795800
buy_quantity: 421000
sell_quantity: 398700
open: 9702.0
high: 9745.3
low: 9688.1
reported_close: 9722.0
oi: 1809500
oi_day_high: None
oi_day_low: None
last_trade_time: 1789492558.0
exchange_time: None
bids: [(9731.3, 300, 2), (9731.2, 1100, 4)]
asks: [(9731.5, 500, 2), (9731.6, 800, 3)]
reported_close when received after the close: None
Methods:
| Name | Description |
|---|---|
feed_key |
The broker token and segments for a Dhan |
tick_spelling |
A Dhan subscription token as ticks carry it: the segment by number rather than name. |
quantity_basis |
Whether the broker reports a quantity field in lots or units on an exchange. |
close_policy |
When the broker's |
normalize |
Apply a plan to one tick. |
Source code in stock_brokers/instruments/ticks/dhan.py
feed_key
¶
The broker token and segments for a Dhan SEGMENT:SECURITY_ID token.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument_token
|
str
|
The tick's |
required |
Returns:
| Type | Description |
|---|---|
FeedKey | None
|
The key, or None for a malformed token or an unknown segment. |
Example usage
Source code in stock_brokers/instruments/ticks/dhan.py
tick_spelling
¶
A Dhan subscription token as ticks carry it: the segment by number rather than name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
subscription_token
|
str
|
A member of |
required |
Returns:
| Type | Description |
|---|---|
str | None
|
The tick's spelling, such as "5:565899", or None for an unknown segment. |
Example usage
Source code in stock_brokers/instruments/ticks/dhan.py
quantity_basis
¶
Whether the broker reports a quantity field in lots or units on an exchange.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
exchange
|
str
|
Canonical exchange. |
required |
field
|
str
|
One of QUANTITY_FIELDS. |
required |
Returns:
| Type | Description |
|---|---|
str
|
BASIS_UNITS, BASIS_LOTS or BASIS_BROKER_LOTS. |
Example usage
Source code in stock_brokers/instruments/ticks/base.py
close_policy
¶
When the broker's close is the previous session's close on an exchange.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
exchange
|
str
|
Canonical exchange. |
required |
Returns:
| Type | Description |
|---|---|
str
|
One of the CLOSE_* policies. |
Example usage
print(f'{exchange} close policy: {self.normalizer.close_policy(exchange)}')
return InstrumentPlan(
instrument_id='11111111-1111-5111-8111-000000000102',
broker='imaginary',
broker_token='2885',
exchange=exchange,
segment=segment,
shape=shape,
session=sessions.session_for(segment),
lot_size=lot_size,
multipliers=multipliers,
price_decimals=2,
negative_prices=shape != 'security' and exchange == 'mcx',
close_policy=self.normalizer.close_policy(exchange),
has_open_interest=shape != 'security',
trusts_last_trade_time=True,
trusts_exchange_time=True,
identity_json='"instrument_id":"11111111-1111-5111-8111-000000000102"',
)
Source code in stock_brokers/instruments/ticks/base.py
normalize
¶
Apply a plan to one tick.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tick
|
dict
|
A tick as the broker's market feed published it. |
required |
plan
|
InstrumentPlan
|
The compiled plan for the tick's token. |
required |
before_trading_close
|
bool
|
Whether the tick was received before its session's close, for the close policy. |
required |
Returns:
| Type | Description |
|---|---|
dict | None
|
The normalized values, or None when the tick carries no usable last price. Keys are last_price, last_quantity, average_price, volume, buy_quantity, sell_quantity, open, high, low, reported_close, oi, oi_day_high, oi_day_low, last_trade_time, exchange_time, bids and asks, where bids and asks are lists of (price, quantity, orders) best first. |
Example usage
values = self.normalizer.normalize(
{
'last_price': -12.5,
'volume': 400,
'oi': 900,
'received_at': self.received_at,
},
future,
True,
)
outcome = self.normalizer.normalize(
{
'last_price': last_price,
'received_at': self.received_at,
},
share,
True,
)
Source code in stock_brokers/instruments/ticks/base.py
_instant
¶
A broker timestamp as a true epoch, or None when it is missing or implausible.
Brokers that stamp times on a shifted clock have that corrected in their market feed; what is left to catch here is a clock nobody has corrected yet, which shows as an instant days away from when the tick arrived.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
float | int | None
|
Epoch seconds as the feed published them. |
required |
received_at
|
float
|
When the feed decoded the tick. |
required |
Returns:
| Type | Description |
|---|---|
float | None
|
The instant, or None. |