shoonya
¶
Shoonya ticks. Shoonya is Finvasia's Noren deployment, so everything is NorenTickNormalizer.
Classes:
| Name | Description |
|---|---|
ShoonyaTickNormalizer |
Normalizes Shoonya ticks. |
ShoonyaTickNormalizer
¶
Bases: NorenTickNormalizer
Normalizes Shoonya ticks.
Example programs
"""Places Shoonya feed tokens, spelled `EXCHANGE|TOKEN`, on the canonical segments they can be in.
Shoonya runs on the Noren platform, so it inherits everything from `NorenTickNormalizer` and only sets its broker name. The exchange before the bar decides the family: MCX and NCX (NCDEX) are commodity derivatives, BFO is BSE derivatives, and BSE is BSE shares and indices together. A token without the bar, or with no exchange before it, prints `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`.
Run it from the project root:
python examples/stock_brokers/instruments/ticks/shoonya/ShoonyaTickNormalizer/example_1_placing_feed_tokens.py
"""
from stock_brokers.instruments.ticks.shoonya import (
ShoonyaTickNormalizer,
)
class PlacingFeedTokensExample:
"""Asks the Shoonya normalizer where each of a few feed tokens belongs.
Attributes:
normalizer (ShoonyaTickNormalizer): The normalizer being shown.
tokens (list): The tokens to place, spelled as the feed spells them.
"""
def __init__(self):
"""Builds the normalizer and the tokens to place.
Returns:
None: This method returns nothing.
"""
self.normalizer = ShoonyaTickNormalizer()
self.tokens = [
'MCX|440001',
'NCX|12345',
'BFO|1135101',
'BSE|500325',
'NSE 2885',
'|2885',
]
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: {ShoonyaTickNormalizer.BROKER_NAME}')
for token in self.tokens:
feed_key = self.normalizer.feed_key(token)
print(f'{token!r} -> {self.describe(feed_key)}')
if __name__ == '__main__':
PlacingFeedTokensExample().run()
It prints:
Broker: shoonya
'MCX|440001' -> broker_token='440001' order_symbol=None segments=16 starting with mcx_fixed_income_futures
'NCX|12345' -> broker_token='12345' order_symbol=None segments=16 starting with ncdex_fixed_income_futures
'BFO|1135101' -> broker_token='1135101' order_symbol=None segments=16 starting with bse_fixed_income_futures
'BSE|500325' -> broker_token='500325' order_symbol=None segments=10 starting with bse_equities
'NSE 2885' -> None
'|2885' -> None
"""Normalizes one Shoonya tick for an MCX natural gas future, whose quantities arrive in lots.
Noren reports MCX quantities in lots, so the plan multiplies each by the contract's 1250-unit lot. Commodity futures have traded below zero, so the plan for an MCX future allows negative prices. Noren's `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/shoonya/ShoonyaTickNormalizer/example_2_mcx_quantities_in_lots.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.shoonya import (
ShoonyaTickNormalizer,
)
from stock_brokers.instruments.ticks.utilities import sessions
INDIA = zoneinfo.ZoneInfo('Asia/Kolkata')
class NaturalGasTickExample:
"""Normalizes one Shoonya tick through a hand-built plan and prints the result.
Attributes:
normalizer (ShoonyaTickNormalizer): 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 = ShoonyaTickNormalizer()
received = datetime.datetime(2026, 9, 15, 21, 30, tzinfo=INDIA)
self.received_at = received.timestamp()
self.tick = {
'instrument_token': 'MCX|440001',
'last_price': 268.4,
'last_quantity': 1,
'average_price': 267.9,
'volume': 88214,
'buy_quantity': 1520,
'sell_quantity': 1733,
'ohlc': {
'open': 265.1,
'high': 270.2,
'low': 264.8,
'close': 266.3,
},
'oi': 21440,
'oi_day_high': 22010,
'oi_day_low': 20985,
'last_trade_time': self.received_at - 2.0,
'exchange_timestamp': self.received_at - 1.0,
'received_at': self.received_at,
'depth': {
'buy': [
{
'price': 268.3,
'quantity': 4,
'orders': 3,
},
{
'price': 268.2,
'quantity': 9,
'orders': 5,
},
],
'sell': [
{
'price': 268.4,
'quantity': 2,
'orders': 1,
},
{
'price': 268.5,
'quantity': 7,
'orders': 4,
},
],
},
}
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 = 1250
broker_lot_size = 1250
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': 'NATURALGAS26SEPFUT',
'underlying_symbol': 'NATURALGAS',
'expiry_date': '2026-09-25',
'strike_price': None,
'option_type': None,
}
instrument_id = '11111111-1111-5111-8111-000000000103'
broker_token = '440001'
return InstrumentPlan(
instrument_id=instrument_id,
broker=ShoonyaTickNormalizer.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=ShoonyaTickNormalizer.TRUSTS_LAST_TRADE_TIME,
trusts_exchange_time=ShoonyaTickNormalizer.TRUSTS_EXCHANGE_TIME,
identity_json=base.identity_json(
instrument_id,
ShoonyaTickNormalizer.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__':
NaturalGasTickExample().run()
It prints:
Token 'MCX|440001': broker_token='440001' order_symbol=None
Volume on mcx is reported in lots, multiplier 1250
Close policy on mcx: before
Normalized, received before the close:
last_price: 268.4
last_quantity: 1250
average_price: 267.9
volume: 110267500
buy_quantity: 1900000
sell_quantity: 2166250
open: 265.1
high: 270.2
low: 264.8
reported_close: 266.3
oi: 26800000
oi_day_high: 27512500
oi_day_low: 26231250
last_trade_time: 1789487998.0
exchange_time: 1789487999.0
bids: [(268.3, 5000, 3), (268.2, 11250, 5)]
asks: [(268.4, 2500, 1), (268.5, 8750, 4)]
reported_close when received after the close: None
Build the lookup from each Noren exchange name to the canonical segments it can hold.
The table is worked out once here from EXCHANGES, so feed_key only has to look a tick's exchange name up rather than recompute its segments on every call.
Example programs
"""Places a token from every exchange the Noren platform names on the canonical segments it can be in.
Flattrade and Shoonya both run on the Noren platform, and `NorenTickNormalizer` holds everything they share: a token is `EXCHANGE|TOKEN`, and the exchange name decides where the instrument is searched for. NSE and BSE hold shares and indices together; NFO, CDS and NCO are NSE's equity, currency and commodity derivatives; BFO and BCD are BSE's; MCX is MCX; and NCDEX is spelt either NCX or NCDEX. The exchange is read without regard to case and surrounding spaces are ignored.
The shared class sets no broker name, because each broker's subclass does; it is used here directly because placing a token does not depend on which Noren broker sent it. The normalizer only works on the text of each token, so no data store, network or stand-in is needed. Each line shows the token, the broker token the mappings are searched with, and how many canonical segments are searched, with the first of them. A token the normalizer cannot place prints `None`.
Run it from the project root:
python examples/stock_brokers/instruments/ticks/noren/NorenTickNormalizer/example_1_every_noren_exchange.py
"""
from stock_brokers.instruments.ticks.noren import (
NorenTickNormalizer,
)
class EveryNorenExchangeExample:
"""Places one token from each Noren exchange.
Attributes:
normalizer (NorenTickNormalizer): The normalizer being shown.
tokens (list): The tokens to place, spelled as a Noren feed spells them.
"""
def __init__(self):
"""Builds the normalizer and the tokens to place.
Returns:
None: This method returns nothing.
"""
self.normalizer = NorenTickNormalizer()
self.tokens = [
'NSE|2885',
'BSE|500325',
'NFO|35003',
'CDS|1573',
'NCO|2003',
'BFO|1135101',
'BCD|8001',
'MCX|440001',
'NCX|12345',
'NCDEX|12345',
' mcx | 440001 ',
'LME|1',
'NSE|',
]
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} segments={segment_count} starting with {first_segment}'
def run(self):
"""Prints where each token belongs.
Returns:
None: This method returns nothing.
"""
print(f'Broker name on the shared class: {NorenTickNormalizer.BROKER_NAME}')
for token in self.tokens:
feed_key = self.normalizer.feed_key(token)
print(f'{token!r} -> {self.describe(feed_key)}')
if __name__ == '__main__':
EveryNorenExchangeExample().run()
It prints:
Broker name on the shared class: None
'NSE|2885' -> broker_token='2885' segments=10 starting with nse_equities
'BSE|500325' -> broker_token='500325' segments=10 starting with bse_equities
'NFO|35003' -> broker_token='35003' segments=16 starting with nse_fixed_income_futures
'CDS|1573' -> broker_token='1573' segments=16 starting with nse_fixed_income_futures
'NCO|2003' -> broker_token='2003' segments=16 starting with nse_fixed_income_futures
'BFO|1135101' -> broker_token='1135101' segments=16 starting with bse_fixed_income_futures
'BCD|8001' -> broker_token='8001' segments=16 starting with bse_fixed_income_futures
'MCX|440001' -> broker_token='440001' segments=16 starting with mcx_fixed_income_futures
'NCX|12345' -> broker_token='12345' segments=16 starting with ncdex_fixed_income_futures
'NCDEX|12345' -> broker_token='12345' segments=16 starting with ncdex_fixed_income_futures
' mcx | 440001 ' -> broker_token='440001' segments=16 starting with mcx_fixed_income_futures
'LME|1' -> None
'NSE|' -> None
"""Adds a normalizer for a new Noren broker in one line, and normalizes an NCDEX tick with it.
A broker that runs on the Noren platform needs nothing of its own but a name: `FlattradeTickNormalizer` and `ShoonyaTickNormalizer` are both a subclass of `NorenTickNormalizer` that sets `BROKER_NAME`. This program does the same for an imaginary Noren broker, then uses what it inherits: `feed_key` places an NCDEX token, `quantity_basis` says NCDEX quantities arrive in lots, and `close_policy` says `close` is the previous close only before the session ends.
The plan that `normalize` applies is normally compiled by `TickResolver` from the mapping cache; here it is built by hand with the same rules, for an NCDEX guar seed future with a lot of 5 units, so no database is needed. The tick is received at 20:00 India time, inside NCDEX's evening session, and is normalized twice, as received before the session's close and after it, which only changes whether `reported_close` is kept.
Run it from the project root:
python examples/stock_brokers/instruments/ticks/noren/NorenTickNormalizer/example_2_a_new_noren_broker.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.noren import (
NorenTickNormalizer,
)
from stock_brokers.instruments.ticks.utilities import sessions
INDIA = zoneinfo.ZoneInfo('Asia/Kolkata')
class ImaginaryNorenTickNormalizer(NorenTickNormalizer):
"""Normalizes ticks from an imaginary broker on the Noren platform."""
BROKER_NAME = 'imaginary_noren'
class NewNorenBrokerExample:
"""Normalizes one NCDEX tick with the new broker's normalizer.
Attributes:
normalizer (ImaginaryNorenTickNormalizer): 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.
"""
def __init__(self):
"""Builds the normalizer and the tick.
Returns:
None: This method returns nothing.
"""
self.normalizer = ImaginaryNorenTickNormalizer()
received = datetime.datetime(2026, 9, 15, 20, 0, tzinfo=INDIA)
self.received_at = received.timestamp()
self.tick = {
'instrument_token': 'NCX|12345',
'last_price': 5412.0,
'last_quantity': 2,
'volume': 3120,
'ohlc': {
'open': 5390.0,
'high': 5430.0,
'low': 5381.0,
'close': 5377.0,
},
'oi': 14880,
'last_trade_time': self.received_at - 3.0,
'exchange_timestamp': self.received_at - 1.0,
'received_at': self.received_at,
}
def build_plan(self, broker_token):
"""Compiles the plan for the guar seed future the way the resolver would.
Args:
broker_token (str): The token the mappings were searched with.
Returns:
InstrumentPlan: The plan.
"""
exchange = 'ncdex'
segment = 'ncdex_commodity_futures'
lot_size = 5
multipliers = {}
for field in base.QUANTITY_FIELDS:
if self.normalizer.quantity_basis(exchange, field) == base.BASIS_LOTS:
multipliers[field] = lot_size
else:
multipliers[field] = 1
identity = {
'exchange': exchange,
'segment': segment,
'shape': 'future',
'symbol': 'GUARSEED10OCT2026',
}
instrument_id = '11111111-1111-5111-8111-000000000110'
return InstrumentPlan(
instrument_id=instrument_id,
broker=ImaginaryNorenTickNormalizer.BROKER_NAME,
broker_token=broker_token,
exchange=exchange,
segment=segment,
shape='future',
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=True,
trusts_last_trade_time=True,
trusts_exchange_time=True,
identity_json=base.identity_json(
instrument_id,
ImaginaryNorenTickNormalizer.BROKER_NAME,
broker_token,
identity,
lot_size,
),
)
def run(self):
"""Prints the feed key, the rules and the normalized values.
Returns:
None: This method returns nothing.
"""
feed_key = self.normalizer.feed_key(self.tick['instrument_token'])
print(f'Broker: {ImaginaryNorenTickNormalizer.BROKER_NAME}')
print(f'Token {self.tick["instrument_token"]!r}: broker_token {feed_key.broker_token!r}, {len(feed_key.segments)} segments starting with {feed_key.segments[0]}')
print(f"NCDEX volume is reported in {self.normalizer.quantity_basis('ncdex', 'volume')}")
print(f"NCDEX close policy: {self.normalizer.close_policy('ncdex')}")
plan = self.build_plan(feed_key.broker_token)
before_close = self.normalizer.normalize(self.tick, plan, True)
after_close = self.normalizer.normalize(self.tick, plan, False)
print(f"last_price {before_close['last_price']}, last_quantity {before_close['last_quantity']}, volume {before_close['volume']}, oi {before_close['oi']}")
print(f"reported_close before the close {before_close['reported_close']}, after it {after_close['reported_close']}")
gate = sessions.SessionGate(sessions.TradingCalendar.empty())
print(f'Received inside the NCDEX window: {gate.in_window("ncdex", plan.session, self.received_at)}')
if __name__ == '__main__':
NewNorenBrokerExample().run()
It prints:
Broker: imaginary_noren
Token 'NCX|12345': broker_token '12345', 16 segments starting with ncdex_fixed_income_futures
NCDEX volume is reported in lots
NCDEX close policy: before
last_price 5412.0, last_quantity 10, volume 15600, oi 74400
reported_close before the close 5377.0, after it None
Received inside the NCDEX window: True
Methods:
| Name | Description |
|---|---|
feed_key |
The broker token and segments for a Noren |
tick_spelling |
A subscription-set token spelled the way the feed spells it on ticks. |
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/noren.py
feed_key
¶
The broker token and segments for a Noren EXCHANGE|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 exchange. |
Example usage
feed_key = self.normalizer.feed_key(self.tick['instrument_token'])
Source code in stock_brokers/instruments/ticks/noren.py
tick_spelling
¶
A subscription-set token spelled the way the feed spells it on ticks.
Plans are filed under the tick's spelling, because that is what the per-tick path looks up. A subscription set holds strings in the spelling the broker's quote feed subscribes with, which is not always the tick's: Kite ticks carry an integer, Dhan ticks a numeric segment code.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
subscription_token
|
str
|
A member of |
required |
Returns:
| Type | Description |
|---|---|
int | str | None
|
The tick's spelling, or None for a token this normalizer cannot place. |
Example usage
print(f"Tick spelling of 'NSE-2885': {self.normalizer.tick_spelling('NSE-2885')!r}")
Source code in stock_brokers/instruments/ticks/base.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. |