shoonya
¶
Shoonya's two websockets on the Noren platform: the market feed and the account's order updates.
Both connect to wss://api.shoonya.com/NorenWSTP/, a separate connection each, so a market data reconnect can never drop an order event.
The login.
ShoonyaSession holds the ShoonyaAPI both sockets read their credentials from, afresh on every connect, so a login made by any process is picked up.
Shoonya's login is a headless Chrome session, so the quote sockets log in again one at a time, and only when no other socket or process has already replaced the token that failed.
The order socket logs in again whenever it is refused, without that check, as it always has.
The connect frame.
Noren streams JSON text, and a connection is unusable until it is authenticated.
On open a socket sends {"t": "a", "uid", "actid", "source": "API", "accesstoken"} with the account's UCC code as the user id.
The frame is a with the token named accesstoken: Noren also parses a c frame naming it susertoken, and refuses a perfectly good token that way, which reads exactly like an expired session.
Only after the ak acknowledgement says OK (documented as "Ok" and seen as "OK", so compared without case) does the socket subscribe; any other answer is a refused login.
Noren's own heartbeat, {"t":"h"}, is sent every three seconds as the ping payload, because a plain websocket ping is not enough to keep the connection.
Quotes.
ShoonyaQuotesSocket subscribes its batch with one {"t": "d", "k": "NSE|2885#MCX|565899"} frame, for depth and touchline together.
The feed is incremental: an acknowledgement (tk, dk) carries every field and each update after it (tf, df) only what changed, so the last state of each instrument is kept and every update merged into it before a tick is built.
Every value arrives as a string and is converted, with blanks and NA as None.
An acknowledgement names the instrument with its trading symbol; a name the socket did not already have is handed to on_name before the tick.
Noren spells ltt as an epoch, a date and time, or a bare India clock time; all three become an epoch, a bare clock time dated by the tick's feed time and moved back a day when it would otherwise be after it, which happens for a trade just before midnight reported just after.
No tick is handed on until an instrument has a price.
Order updates.
ShoonyaOrderUpdatesSocket subscribes to the whole account with {"t": "o", "actid": <ucc code>}, and every change to an order then arrives as an om message carrying the order book's fields, every value a string.
Neither socket writes Redis.
The quotes socket hands names to on_name and each tick to on_tick, and the order socket hands each message's om updates to on_updates; the scripts in bin/shoonya/ do the writing.
Classes:
| Name | Description |
|---|---|
ShoonyaSession |
The Shoonya login every socket authenticates with, logged in again by one socket at a time. |
ShoonyaQuotesSocket |
One Noren market feed websocket carrying the touchline and depth of a batch of instruments. |
ShoonyaOrderUpdatesSocket |
Shoonya's order update websocket, subscribed to the whole account once authenticated. |
ShoonyaSession
¶
The Shoonya login every socket authenticates with, logged in again by one socket at a time.
Constructs ShoonyaAPI, which logs in when the stored session is dead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
logger
|
Logger
|
Where logins are reported. |
required |
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Raises:
| Type | Description |
|---|---|
Exception
|
Whatever |
Example programs
"""Builds a Shoonya session, reads its credentials and logs in again only when the failed token is still current.
Shoonya's login drives a headless Chrome session, so `ShoonyaSession` makes sure refused sockets log in one at a time: `log_in_again` logs in only when the token a refused socket used is still the one in force, so several refused sockets cause one login between them. `credentials` returns the account's UCC code, which Noren takes as the user id, and the token in force now.
Constructing the real `ShoonyaAPI` logs in to Shoonya, so the program registers a stand-in module under `stock_brokers.api.shoonya`. Each construction of the stand-in counts as a login and issues the next numbered token, `_current_login` answers from a shared record standing in for the Redis `last_login` hash.
Notice that the second request with the same stale token makes no login.
Run it from the project root:
python examples/stock_brokers/websockets/shoonya/ShoonyaSession/example_1_logging_in_again_once.py
"""
import logging
import sys
import types
from stock_brokers.websockets.shoonya import (
ShoonyaSession,
)
class LoginRecord:
"""The Shoonya login state every stand-in API object reads, standing in for the Redis `last_login` hash.
Attributes:
access_token (str | None): The token in force now.
login_count (int): How many logins the stand-in API has made.
"""
access_token = None
login_count = 0
class StandInShoonyaAPI:
"""A stand-in for `ShoonyaAPI` whose construction is a pretend login."""
def __init__(self):
"""Logs in, which here only issues the next numbered token.
Returns:
None: This method returns nothing.
"""
LoginRecord.login_count = LoginRecord.login_count + 1
LoginRecord.access_token = f'shoonya-token-{LoginRecord.login_count}'
print(f'Stand-in login number {LoginRecord.login_count}')
self._settings = {
'ucc_code': 'FA12345',
}
def _current_login(self):
"""The login in force now, as the real class reads it from Redis.
Returns:
dict | None: The access token, or None before any login.
"""
if LoginRecord.access_token is None:
return None
return {
'access_token': LoginRecord.access_token,
}
class LoggingInAgainOnceExample:
"""Builds a Shoonya session and asks it to log in again twice with the same token.
Attributes:
session (ShoonyaSession): The session being shown.
"""
def __init__(self):
"""Registers the stand-in API module and builds the session.
Returns:
None: This method returns nothing.
"""
logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
api_module = types.ModuleType('stock_brokers.api.shoonya')
api_module.ShoonyaAPI = StandInShoonyaAPI
sys.modules['stock_brokers.api.shoonya'] = api_module
self.session = ShoonyaSession(logging.getLogger('shoonya.session'))
def run(self):
"""Refuses the same token twice and prints the credentials after each request.
Returns:
None: This method returns nothing.
"""
user_id, failed_token = self.session.credentials()
print(f'User {user_id} was refused with {failed_token}')
self.session.log_in_again(failed_token)
print(f'Credentials now: {self.session.credentials()}')
self.session.log_in_again(failed_token)
print(f'Credentials after the second request: {self.session.credentials()}')
print(f'Logins made: {LoginRecord.login_count}')
if __name__ == '__main__':
LoggingInAgainOnceExample().run()
It prints:
"""Logs in to Shoonya again unconditionally, as the order update socket does after a refusal.
The order update socket calls `ShoonyaSession.log_in_again_without_checking` when Noren refuses its connect frame. That logs in even when another process has already replaced the token, unlike `log_in_again`, which skips the login in that case.
Constructing the real `ShoonyaAPI` logs in to Shoonya through a headless Chrome session, so the program registers a stand-in module under `stock_brokers.api.shoonya` whose constructions count as logins. It first sets the token as another process would, shows that `log_in_again` with the old token does nothing, and then calls `log_in_again_without_checking`.
Notice that only the unconditional call makes a login, and that it replaces the other process's token with a new one.
Run it from the project root:
python examples/stock_brokers/websockets/shoonya/ShoonyaSession/example_2_logging_in_without_checking.py
"""
import logging
import sys
import types
from stock_brokers.websockets.shoonya import (
ShoonyaSession,
)
class LoginRecord:
"""The Shoonya login state every stand-in API object reads, standing in for the Redis `last_login` hash.
Attributes:
access_token (str | None): The token in force now.
login_count (int): How many logins the stand-in API has made.
"""
access_token = None
login_count = 0
class StandInShoonyaAPI:
"""A stand-in for `ShoonyaAPI` whose construction is a pretend login."""
def __init__(self):
"""Logs in, which here only issues the next numbered token.
Returns:
None: This method returns nothing.
"""
LoginRecord.login_count = LoginRecord.login_count + 1
LoginRecord.access_token = f'shoonya-token-{LoginRecord.login_count}'
print(f'Stand-in login number {LoginRecord.login_count}')
self._settings = {
'ucc_code': 'FA12345',
}
def _current_login(self):
"""The login in force now, as the real class reads it from Redis.
Returns:
dict | None: The access token, or None before any login.
"""
if LoginRecord.access_token is None:
return None
return {
'access_token': LoginRecord.access_token,
}
class LoggingInWithoutCheckingExample:
"""Compares the checked and unchecked logins of a Shoonya session.
Attributes:
session (ShoonyaSession): The session being shown.
"""
def __init__(self):
"""Registers the stand-in API module and builds the session.
Returns:
None: This method returns nothing.
"""
logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
api_module = types.ModuleType('stock_brokers.api.shoonya')
api_module.ShoonyaAPI = StandInShoonyaAPI
sys.modules['stock_brokers.api.shoonya'] = api_module
self.session = ShoonyaSession(logging.getLogger('shoonya.session'))
def run(self):
"""Replaces the token elsewhere, then tries both kinds of login.
Returns:
None: This method returns nothing.
"""
first_token = self.session.credentials()[1]
LoginRecord.access_token = 'shoonya-token-from-another-process'
print(f'Another process replaced {first_token}.')
self.session.log_in_again(first_token)
print(f'After log_in_again: {self.session.credentials()[1]}')
self.session.log_in_again_without_checking()
print(f'After log_in_again_without_checking: {self.session.credentials()[1]}')
print(f'Logins made: {LoginRecord.login_count}')
if __name__ == '__main__':
LoggingInWithoutCheckingExample().run()
It prints:
Methods:
| Name | Description |
|---|---|
credentials |
The Noren user id and the session token in force now, which may be one another process has just obtained. |
log_in_again |
Logs in again, unless another socket or process already replaced the token that failed. |
log_in_again_without_checking |
Logs in again whether or not the token was already replaced. |
Source code in stock_brokers/websockets/shoonya.py
credentials
¶
The Noren user id and the session token in force now, which may be one another process has just obtained.
Returns:
| Type | Description |
|---|---|
tuple
|
The UCC code and the session token, either of which may be None. |
Example usage
print(f'Credentials after the second request: {self.session.credentials()}')
Source code in stock_brokers/websockets/shoonya.py
log_in_again
¶
Logs in again, unless another socket or process already replaced the token that failed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stale_token
|
str | None
|
The session token the failing connection used. |
required |
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Raises:
| Type | Description |
|---|---|
Exception
|
Whatever |
Example usage
Source code in stock_brokers/websockets/shoonya.py
log_in_again_without_checking
¶
Logs in again whether or not the token was already replaced.
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Raises:
| Type | Description |
|---|---|
Exception
|
Whatever |
Example usage
Source code in stock_brokers/websockets/shoonya.py
_log_in
¶
Logs in by constructing ShoonyaAPI again; the caller holds the lock.
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Raises:
| Type | Description |
|---|---|
Exception
|
Whatever |
Source code in stock_brokers/websockets/shoonya.py
ShoonyaQuotesSocket
¶
Bases: BrokerWebsocket
One Noren market feed websocket carrying the touchline and depth of a batch of instruments.
Sets up a socket for one batch of instruments.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
The connection's name, for the log. |
required |
tokens
|
list[str]
|
The batch of |
required |
names
|
dict[str, str]
|
Each token to the instrument's name, shared between sockets and added to as Noren names instruments. |
required |
session
|
ShoonyaSession
|
The shared login. |
required |
on_name
|
callable
|
Called with a token and its new name when an acknowledgement names an instrument, on this socket's thread. |
required |
on_tick
|
callable
|
Called with each tick, on this socket's thread. |
required |
logger
|
Logger
|
Where the connection reports. |
required |
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Example programs
"""Authenticates a Shoonya quotes socket and merges Noren's incremental feed into full ticks.
A `ShoonyaQuotesSocket` sends Noren's connect frame when the connection opens and subscribes its batch only after the `ak` acknowledgement says `OK`. The feed is incremental: a touchline or depth acknowledgement (`tk`, `dk`) carries every field and names the instrument, and each update after it (`tf`, `df`) carries only what changed. The socket keeps the last state of each instrument and merges every update into it before building a tick, with every string converted to a number.
This program scripts one connection that answers the connect frame with `OK`, sends a depth acknowledgement for NSE token 2885 naming it RELIANCE-EQ, and then a depth update that changes only the last price and the best bid. No real socket is opened: a stand-in `websocket` package plays the frames and prints what the socket sends, and a stand-in `stock_brokers.api.shoonya` module lets the real `ShoonyaSession` log in without reaching Shoonya.
Notice that the new name is handed to `on_name` before the first tick is handed to `on_tick`, and that the second tick still has the open, high, low, volume and trade time from the acknowledgement although the update did not repeat them. The trade time `25-09-2026 10:15:28` becomes an epoch.
Run it from the project root:
python examples/stock_brokers/websockets/shoonya/ShoonyaQuotesSocket/example_1_merging_incremental_updates.py
"""
import json
import logging
import sys
import types
from stock_brokers.websockets.shoonya import (
ShoonyaQuotesSocket,
ShoonyaSession,
)
class LoginRecord:
"""The Shoonya login state every stand-in API object reads, standing in for the Redis `last_login` hash.
Attributes:
access_token (str | None): The token in force now.
login_count (int): How many logins the stand-in API has made.
"""
access_token = None
login_count = 0
class StandInShoonyaAPI:
"""A stand-in for `ShoonyaAPI` whose construction is a pretend login."""
def __init__(self):
"""Logs in, which here only issues the next numbered token.
Returns:
None: This method returns nothing.
"""
LoginRecord.login_count = LoginRecord.login_count + 1
LoginRecord.access_token = f'shoonya-token-{LoginRecord.login_count}'
print(f'Stand-in login number {LoginRecord.login_count}')
self._settings = {
'ucc_code': 'FA12345',
}
def _current_login(self):
"""The login in force now, as the real class reads it from Redis.
Returns:
dict | None: The access token, or None before any login.
"""
if LoginRecord.access_token is None:
return None
return {
'access_token': LoginRecord.access_token,
}
class HandshakeRefused(Exception):
"""A refused websocket handshake, carrying the HTTP status the way `websocket-client` does.
Attributes:
status_code (int): The HTTP status the broker answered with.
"""
def __init__(self, status_code):
"""Keeps the status.
Args:
status_code (int): The HTTP status the broker answered with.
Returns:
None: This method returns nothing.
"""
super().__init__(f'Handshake status {status_code}')
self.status_code = status_code
class ScriptedConnection:
"""A stand-in for `websocket.WebSocketApp` that either refuses the handshake or plays frames to the socket's handlers.
Attributes:
url (str): The URL the socket asked for.
plan (dict): Either a `refuse_with` status or a list of `frames`.
handlers (dict): The socket's callbacks, by name.
closed (bool): Whether the socket asked to close, after which no more frames are delivered.
"""
def __init__(self, url, plan, handlers):
"""Keeps what the socket passed in.
Args:
url (str): The URL the socket asked for.
plan (dict): Either a `refuse_with` status or a list of `frames`.
handlers (dict): The socket's callbacks, by name.
Returns:
None: This method returns nothing.
"""
self.url = url
self.plan = plan
self.handlers = handlers
self.closed = False
def run_forever(self, **options):
"""Plays the plan and returns when the pretend connection closes.
Args:
**options (dict): The ping and TLS settings the socket asks for.
Returns:
None: This method returns nothing.
"""
print(f'Connecting to {self.url}')
if 'refuse_with' in self.plan:
self.handlers['on_error'](self, HandshakeRefused(self.plan['refuse_with']))
self.handlers['on_close'](self, None, None)
return
self.handlers['on_open'](self)
for frame in self.plan['frames']:
if self.closed:
break
self.handlers['on_message'](self, frame)
self.handlers['on_close'](self, 1000, 'normal closure')
def send(self, data, opcode=None):
"""Prints a message the socket sends to the broker.
Args:
data (str | bytes): The message.
opcode (int | None): The frame type, when the socket names one.
Returns:
None: This method returns nothing.
"""
print(f'Sent: {data}')
def close(self, **options):
"""Notes that the socket asked to close.
Args:
**options (dict): Any close status the socket passes.
Returns:
None: This method returns nothing.
"""
self.closed = True
class ScriptedWebsocketModule:
"""A stand-in for the `websocket` package that hands out scripted connections in turn.
Attributes:
plans (list): What each connection does, in the order the socket connects.
when_finished (callable | None): Called when the last connection is handed out, so the socket stops after it.
"""
def __init__(self, plans):
"""Keeps the connection plans.
Args:
plans (list): What each connection does, in the order the socket connects.
Returns:
None: This method returns nothing.
"""
self.plans = plans
self.when_finished = None
def WebSocketApp(self, url, **handlers):
"""Builds the next scripted connection, closing the socket after the last one.
Args:
url (str): The feed URL.
**handlers (dict): The socket's `on_open`, `on_message`, `on_error` and `on_close` callbacks.
Returns:
ScriptedConnection: The connection.
"""
plan = self.plans.pop(0)
if not self.plans:
self.when_finished()
return ScriptedConnection(url, plan, handlers)
class MergingIncrementalUpdatesExample:
"""Runs a Shoonya quotes socket against an acknowledgement and one update.
Attributes:
socket (ShoonyaQuotesSocket): The socket being shown.
"""
def __init__(self):
"""Builds the frames, the stand-ins, the session and the socket.
Returns:
None: This method returns nothing.
"""
logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
api_module = types.ModuleType('stock_brokers.api.shoonya')
api_module.ShoonyaAPI = StandInShoonyaAPI
sys.modules['stock_brokers.api.shoonya'] = api_module
connect_acknowledgement = {
't': 'ak',
's': 'OK',
'uid': 'FA12345',
}
depth_acknowledgement = {
't': 'dk',
'e': 'NSE',
'tk': '2885',
'ts': 'RELIANCE-EQ',
'lp': '2950.50',
'pc': '1.39',
'c': '2910.00',
'o': '2930.00',
'h': '2960.00',
'l': '2925.00',
'v': '1204500',
'ap': '2949.10',
'ltt': '25-09-2026 10:15:28',
'ft': '1790311529',
'bp1': '2950.00',
'bq1': '100',
'bo1': '3',
'sp1': '2950.50',
'sq1': '120',
'so1': '4',
}
depth_update = {
't': 'df',
'e': 'NSE',
'tk': '2885',
'lp': '2951.00',
'pc': '1.41',
'bp1': '2950.50',
'bq1': '80',
'ft': '1790311530',
}
plans = [
{
'frames': [
json.dumps(connect_acknowledgement),
json.dumps(depth_acknowledgement),
json.dumps(depth_update),
],
},
]
websocket_module = ScriptedWebsocketModule(plans)
sys.modules['websocket'] = websocket_module
logger = logging.getLogger('shoonya.quotes')
tokens = [
'NSE|2885',
]
names = {}
session = ShoonyaSession(logger)
self.socket = ShoonyaQuotesSocket('socket_0', tokens, names, session, self.print_name, self.print_tick, logger)
websocket_module.when_finished = self.socket.close
def print_name(self, token, name):
"""Prints an instrument an acknowledgement named.
Args:
token (str): The instrument's `EXCHANGE|TOKEN`.
name (str): Its new name.
Returns:
None: This method returns nothing.
"""
print(f'Name learned: {token} is {name}')
def print_tick(self, tick):
"""Prints the main fields of one tick the socket built.
Args:
tick (dict): The normalized tick.
Returns:
None: This method returns nothing.
"""
change = tick['change']
if change is not None:
change = round(change, 4)
print(f"Tick {tick['id']} ({tick['instrument_token']}) in {tick['mode']} mode")
print(f" last_price={tick['last_price']} volume={tick['volume']} change={change}")
print(f" ohlc={tick['ohlc']}")
print(f" last_trade_time={tick['last_trade_time']} exchange_timestamp={tick['exchange_timestamp']}")
if tick['depth']['buy']:
print(f" best bid={tick['depth']['buy'][0]} best offer={tick['depth']['sell'][0]}")
def run(self):
"""Runs the socket's reconnect loop until the scripted connection ends.
Returns:
None: This method returns nothing.
"""
self.socket.run_forever()
print(f'Gave up: {self.socket.gave_up}')
if __name__ == '__main__':
MergingIncrementalUpdatesExample().run()
It prints:
Stand-in login number 1
Connecting to wss://api.shoonya.com/NorenWSTP/
Sent: {"t": "a", "uid": "FA12345", "actid": "FA12345", "source": "API", "accesstoken": "shoonya-token-1"}
INFO socket_0 opened. Authenticating.
Sent: {"t": "d", "k": "NSE|2885"}
INFO socket_0 authenticated and subscribed to 1 instrument(s).
Name learned: NSE|2885 is NSE:RELIANCE-EQ
Tick NSE:RELIANCE-EQ (NSE|2885) in full mode
last_price=2950.5 volume=1204500 change=1.39
ohlc={'open': 2930.0, 'high': 2960.0, 'low': 2925.0, 'close': 2910.0}
last_trade_time=1790311528 exchange_timestamp=1790311529
best bid={'quantity': 100, 'price': 2950.0, 'orders': 3} best offer={'quantity': 120, 'price': 2950.5, 'orders': 4}
Tick NSE:RELIANCE-EQ (NSE|2885) in full mode
last_price=2951.0 volume=1204500 change=1.41
ohlc={'open': 2930.0, 'high': 2960.0, 'low': 2925.0, 'close': 2910.0}
last_trade_time=1790311528 exchange_timestamp=1790311530
best bid={'quantity': 80, 'price': 2950.5, 'orders': 3} best offer={'quantity': 120, 'price': 2950.5, 'orders': 4}
INFO socket_0 closed with status 1000: normal closure
INFO socket_0 stopped.
Gave up: False
"""Shows a Shoonya quotes socket logging in again when Noren refuses its connect frame.
Noren accepts every websocket handshake and refuses a dead session in its `ak` acknowledgement instead, with a status other than `OK`. `ShoonyaQuotesSocket` then closes the connection and its reconnect loop asks the shared `ShoonyaSession` to log in again, before the socket connects with the new token.
This program scripts two connections. The first answers the connect frame with `NOT_OK`. The second accepts it and sends a touchline acknowledgement for MCX token 565899 whose trade time is a bare India clock time, `10:15:28`, and a touchline acknowledgement for a token with no price yet. No real socket is opened: a stand-in `websocket` package plays the connections, and a stand-in `stock_brokers.api.shoonya` module counts each `ShoonyaAPI` construction as a login. The backoff is set to zero seconds.
Notice the second login, the new token in the second connect frame, and that the bare clock time is dated by the frame's feed time. The instrument without a price is named but produces no tick.
Run it from the project root:
python examples/stock_brokers/websockets/shoonya/ShoonyaQuotesSocket/example_2_refused_acknowledgement.py
"""
import json
import logging
import sys
import types
from stock_brokers.websockets.shoonya import (
ShoonyaQuotesSocket,
ShoonyaSession,
)
class LoginRecord:
"""The Shoonya login state every stand-in API object reads, standing in for the Redis `last_login` hash.
Attributes:
access_token (str | None): The token in force now.
login_count (int): How many logins the stand-in API has made.
"""
access_token = None
login_count = 0
class StandInShoonyaAPI:
"""A stand-in for `ShoonyaAPI` whose construction is a pretend login."""
def __init__(self):
"""Logs in, which here only issues the next numbered token.
Returns:
None: This method returns nothing.
"""
LoginRecord.login_count = LoginRecord.login_count + 1
LoginRecord.access_token = f'shoonya-token-{LoginRecord.login_count}'
print(f'Stand-in login number {LoginRecord.login_count}')
self._settings = {
'ucc_code': 'FA12345',
}
def _current_login(self):
"""The login in force now, as the real class reads it from Redis.
Returns:
dict | None: The access token, or None before any login.
"""
if LoginRecord.access_token is None:
return None
return {
'access_token': LoginRecord.access_token,
}
class HandshakeRefused(Exception):
"""A refused websocket handshake, carrying the HTTP status the way `websocket-client` does.
Attributes:
status_code (int): The HTTP status the broker answered with.
"""
def __init__(self, status_code):
"""Keeps the status.
Args:
status_code (int): The HTTP status the broker answered with.
Returns:
None: This method returns nothing.
"""
super().__init__(f'Handshake status {status_code}')
self.status_code = status_code
class ScriptedConnection:
"""A stand-in for `websocket.WebSocketApp` that either refuses the handshake or plays frames to the socket's handlers.
Attributes:
url (str): The URL the socket asked for.
plan (dict): Either a `refuse_with` status or a list of `frames`.
handlers (dict): The socket's callbacks, by name.
closed (bool): Whether the socket asked to close, after which no more frames are delivered.
"""
def __init__(self, url, plan, handlers):
"""Keeps what the socket passed in.
Args:
url (str): The URL the socket asked for.
plan (dict): Either a `refuse_with` status or a list of `frames`.
handlers (dict): The socket's callbacks, by name.
Returns:
None: This method returns nothing.
"""
self.url = url
self.plan = plan
self.handlers = handlers
self.closed = False
def run_forever(self, **options):
"""Plays the plan and returns when the pretend connection closes.
Args:
**options (dict): The ping and TLS settings the socket asks for.
Returns:
None: This method returns nothing.
"""
print(f'Connecting to {self.url}')
if 'refuse_with' in self.plan:
self.handlers['on_error'](self, HandshakeRefused(self.plan['refuse_with']))
self.handlers['on_close'](self, None, None)
return
self.handlers['on_open'](self)
for frame in self.plan['frames']:
if self.closed:
break
self.handlers['on_message'](self, frame)
self.handlers['on_close'](self, 1000, 'normal closure')
def send(self, data, opcode=None):
"""Prints a message the socket sends to the broker.
Args:
data (str | bytes): The message.
opcode (int | None): The frame type, when the socket names one.
Returns:
None: This method returns nothing.
"""
print(f'Sent: {data}')
def close(self, **options):
"""Notes that the socket asked to close.
Args:
**options (dict): Any close status the socket passes.
Returns:
None: This method returns nothing.
"""
self.closed = True
class ScriptedWebsocketModule:
"""A stand-in for the `websocket` package that hands out scripted connections in turn.
Attributes:
plans (list): What each connection does, in the order the socket connects.
when_finished (callable | None): Called when the last connection is handed out, so the socket stops after it.
"""
def __init__(self, plans):
"""Keeps the connection plans.
Args:
plans (list): What each connection does, in the order the socket connects.
Returns:
None: This method returns nothing.
"""
self.plans = plans
self.when_finished = None
def WebSocketApp(self, url, **handlers):
"""Builds the next scripted connection, closing the socket after the last one.
Args:
url (str): The feed URL.
**handlers (dict): The socket's `on_open`, `on_message`, `on_error` and `on_close` callbacks.
Returns:
ScriptedConnection: The connection.
"""
plan = self.plans.pop(0)
if not self.plans:
self.when_finished()
return ScriptedConnection(url, plan, handlers)
class RefusedAcknowledgementExample:
"""Runs a Shoonya quotes socket through a refused connect frame and a good connection.
Attributes:
socket (ShoonyaQuotesSocket): The socket being shown.
"""
def __init__(self):
"""Builds the frames, the stand-ins, the session and the socket.
Returns:
None: This method returns nothing.
"""
logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
api_module = types.ModuleType('stock_brokers.api.shoonya')
api_module.ShoonyaAPI = StandInShoonyaAPI
sys.modules['stock_brokers.api.shoonya'] = api_module
refused = {
't': 'ak',
's': 'NOT_OK',
'emsg': 'Session Expired : Invalid Session Key',
}
accepted = {
't': 'ak',
's': 'Ok',
}
crude_touchline = {
't': 'tk',
'e': 'MCX',
'tk': '565899',
'ts': 'CRUDEOIL19OCT26',
'lp': '6120.00',
'c': '6100.00',
'v': '8450',
'oi': '12500',
'ltt': '10:15:28',
'ft': '1790311529',
}
unpriced_touchline = {
't': 'tk',
'e': 'MCX',
'tk': '565900',
'ts': 'CRUDEOIL19NOV26',
'lp': 'NA',
}
plans = [
{
'frames': [
json.dumps(refused),
],
},
{
'frames': [
json.dumps(accepted),
json.dumps(crude_touchline),
json.dumps(unpriced_touchline),
],
},
]
websocket_module = ScriptedWebsocketModule(plans)
sys.modules['websocket'] = websocket_module
logger = logging.getLogger('shoonya.quotes')
tokens = [
'MCX|565899',
'MCX|565900',
]
names = {}
session = ShoonyaSession(logger)
self.socket = ShoonyaQuotesSocket('socket_0', tokens, names, session, self.print_name, self.print_tick, logger)
self.socket.MIN_BACKOFF_SECONDS = 0
websocket_module.when_finished = self.socket.close
def print_name(self, token, name):
"""Prints an instrument an acknowledgement named.
Args:
token (str): The instrument's `EXCHANGE|TOKEN`.
name (str): Its new name.
Returns:
None: This method returns nothing.
"""
print(f'Name learned: {token} is {name}')
def print_tick(self, tick):
"""Prints the main fields of one tick the socket built.
Args:
tick (dict): The normalized tick.
Returns:
None: This method returns nothing.
"""
change = tick['change']
if change is not None:
change = round(change, 4)
print(f"Tick {tick['id']} ({tick['instrument_token']}) in {tick['mode']} mode")
print(f" last_price={tick['last_price']} volume={tick['volume']} change={change}")
print(f" ohlc={tick['ohlc']}")
print(f" last_trade_time={tick['last_trade_time']} exchange_timestamp={tick['exchange_timestamp']}")
if tick['depth']['buy']:
print(f" best bid={tick['depth']['buy'][0]} best offer={tick['depth']['sell'][0]}")
def run(self):
"""Runs the socket's reconnect loop until the scripted connections run out.
Returns:
None: This method returns nothing.
"""
self.socket.run_forever()
print(f'Gave up: {self.socket.gave_up}')
print(f'Logins made: {LoginRecord.login_count}')
if __name__ == '__main__':
RefusedAcknowledgementExample().run()
It prints:
Stand-in login number 1
Connecting to wss://api.shoonya.com/NorenWSTP/
Sent: {"t": "a", "uid": "FA12345", "actid": "FA12345", "source": "API", "accesstoken": "shoonya-token-1"}
INFO socket_0 opened. Authenticating.
ERROR socket_0 authentication refused: {'t': 'ak', 's': 'NOT_OK', 'emsg': 'Session Expired : Invalid Session Key'}
INFO socket_0 closed with status 1000: normal closure
WARNING Logging in to Shoonya again.
Stand-in login number 2
WARNING socket_0 disconnected. Reconnecting in 0 seconds.
Connecting to wss://api.shoonya.com/NorenWSTP/
Sent: {"t": "a", "uid": "FA12345", "actid": "FA12345", "source": "API", "accesstoken": "shoonya-token-2"}
INFO socket_0 opened. Authenticating.
Sent: {"t": "d", "k": "MCX|565899#MCX|565900"}
INFO socket_0 authenticated and subscribed to 2 instrument(s).
Name learned: MCX|565899 is MCX:CRUDEOIL19OCT26
Tick MCX:CRUDEOIL19OCT26 (MCX|565899) in quote mode
last_price=6120.0 volume=8450 change=0.3279
ohlc={'open': None, 'high': None, 'low': None, 'close': 6100.0}
last_trade_time=1790311528 exchange_timestamp=1790311529
Name learned: MCX|565900 is MCX:CRUDEOIL19NOV26
INFO socket_0 closed with status 1000: normal closure
INFO socket_0 stopped.
Gave up: False
Logins made: 2
Methods:
| Name | Description |
|---|---|
close |
Closes the connection and stops reconnecting. |
run_forever |
Connects, and reconnects with backoff, until closed or unable to recover. |
Source code in stock_brokers/websockets/shoonya.py
_connect
¶
Opens the websocket and blocks until it closes, sending Noren's heartbeat meanwhile.
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Source code in stock_brokers/websockets/shoonya.py
_log_in_again
¶
Logs in again through the shared session, which skips the login when the token was already replaced.
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
_on_open
¶
Authenticates with the token in force now; the subscription waits for the acknowledgement.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
websocket_connection
|
WebSocketApp
|
The connection that opened. |
required |
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Source code in stock_brokers/websockets/shoonya.py
_on_error
¶
Reports an error; Noren refuses a login in its acknowledgement rather than at the handshake.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
websocket_connection
|
WebSocketApp
|
The connection that failed. |
required |
error
|
Exception
|
The error. |
required |
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Source code in stock_brokers/websockets/shoonya.py
_on_message
¶
Handles the connect acknowledgement, and hands each tick on.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
websocket_connection
|
WebSocketApp
|
The connection the message arrived on. |
required |
message
|
bytes | str
|
The message. |
required |
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Source code in stock_brokers/websockets/shoonya.py
_parse
¶
Builds a tick from one Noren message, subscribing on the connect acknowledgement and learning names on the way.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
bytes | str
|
The message. |
required |
Returns:
| Type | Description |
|---|---|
dict | None
|
The tick, or None for a message that carries none. |
Source code in stock_brokers/websockets/shoonya.py
_build_tick
¶
Builds a normalized tick from an instrument's merged state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token
|
str
|
The instrument's |
required |
state
|
dict
|
Its merged field state. |
required |
identifier
|
str
|
The name the tick is keyed by. |
required |
Returns:
| Type | Description |
|---|---|
dict
|
The tick. |
Source code in stock_brokers/websockets/shoonya.py
_number
¶
A Noren string field as a number, with blanks and placeholders as None.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
str | None
|
The raw field. |
required |
cast
|
type
|
|
float
|
Returns:
| Type | Description |
|---|---|
float | int | None
|
The number, or None. |
Source code in stock_brokers/websockets/shoonya.py
_trade_time
¶
Noren's last trade time as an epoch, whether sent as an epoch, a date and time, or a bare India clock time.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
str | None
|
The raw |
required |
feed_time
|
int | None
|
The tick's |
required |
Returns:
| Type | Description |
|---|---|
int | None
|
The epoch, or None when the field cannot be read. |
Source code in stock_brokers/websockets/shoonya.py
close
¶
Closes the connection and stops reconnecting.
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Example usage
run_forever
¶
Connects, and reconnects with backoff, until closed or unable to recover.
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Example usage
Source code in stock_brokers/websockets/base.py
_gives_up_after_logging_in_again
¶
Whether a socket that has already logged in again gives up on this failure.
The loop asks only when the connection was refused or has failed six times in a row. By default the answer is yes, so a refusal straight after logging in again ends the socket. A broker whose refusals are not always about the login overrides this to give up only after six more failed connects.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
failed_connects
|
int
|
How many connects in a row have failed since the last login. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True to give up now. |
Source code in stock_brokers/websockets/base.py
_on_close
¶
Reports that the connection closed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
websocket_connection
|
WebSocketApp
|
The connection that closed. |
required |
status_code
|
int | None
|
The close status the broker sent. |
required |
message
|
str | None
|
The close reason the broker sent. |
required |
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Source code in stock_brokers/websockets/base.py
ShoonyaOrderUpdatesSocket
¶
Bases: BrokerWebsocket
Shoonya's order update websocket, subscribed to the whole account once authenticated.
Sets up the order updates socket.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
ShoonyaSession
|
The login. |
required |
on_updates
|
callable
|
Called on this socket's thread with each message's |
required |
logger
|
Logger
|
Where the connection reports. |
required |
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Example programs
"""Authenticates Shoonya's order update socket, subscribes to the account and prints the order updates it hands on.
`ShoonyaOrderUpdatesSocket` sends Noren's connect frame when the connection opens, and once the `ak` acknowledgement says `OK` it subscribes to the whole account with `{"t": "o", "actid": ...}`. Every change to an order then arrives as an `om` message, and the socket hands each frame's `om` updates that carry an order number on, exactly as Noren sent them. Shoonya runs it as a connection of its own, so a market data reconnect can never drop an order event.
This program scripts one connection that sends the acknowledgement and then one frame holding a list of two `om` updates and a message of another type. No real socket is opened: a stand-in `websocket` package plays the frames and prints what the socket sends, and a stand-in `stock_brokers.api.shoonya` module lets the real `ShoonyaSession` log in without reaching Shoonya.
Notice the connect frame and the account subscription in the output, and that both updates of the list reach the callback in one call.
Run it from the project root:
python examples/stock_brokers/websockets/shoonya/ShoonyaOrderUpdatesSocket/example_1_subscribing_to_the_account.py
"""
import json
import logging
import sys
import types
from stock_brokers.websockets.shoonya import (
ShoonyaOrderUpdatesSocket,
ShoonyaSession,
)
class LoginRecord:
"""The Shoonya login state every stand-in API object reads, standing in for the Redis `last_login` hash.
Attributes:
access_token (str | None): The token in force now.
login_count (int): How many logins the stand-in API has made.
"""
access_token = None
login_count = 0
class StandInShoonyaAPI:
"""A stand-in for `ShoonyaAPI` whose construction is a pretend login."""
def __init__(self):
"""Logs in, which here only issues the next numbered token.
Returns:
None: This method returns nothing.
"""
LoginRecord.login_count = LoginRecord.login_count + 1
LoginRecord.access_token = f'shoonya-token-{LoginRecord.login_count}'
print(f'Stand-in login number {LoginRecord.login_count}')
self._settings = {
'ucc_code': 'FA12345',
}
def _current_login(self):
"""The login in force now, as the real class reads it from Redis.
Returns:
dict | None: The access token, or None before any login.
"""
if LoginRecord.access_token is None:
return None
return {
'access_token': LoginRecord.access_token,
}
class HandshakeRefused(Exception):
"""A refused websocket handshake, carrying the HTTP status the way `websocket-client` does.
Attributes:
status_code (int): The HTTP status the broker answered with.
"""
def __init__(self, status_code):
"""Keeps the status.
Args:
status_code (int): The HTTP status the broker answered with.
Returns:
None: This method returns nothing.
"""
super().__init__(f'Handshake status {status_code}')
self.status_code = status_code
class ScriptedConnection:
"""A stand-in for `websocket.WebSocketApp` that either refuses the handshake or plays frames to the socket's handlers.
Attributes:
url (str): The URL the socket asked for.
plan (dict): Either a `refuse_with` status or a list of `frames`.
handlers (dict): The socket's callbacks, by name.
closed (bool): Whether the socket asked to close, after which no more frames are delivered.
"""
def __init__(self, url, plan, handlers):
"""Keeps what the socket passed in.
Args:
url (str): The URL the socket asked for.
plan (dict): Either a `refuse_with` status or a list of `frames`.
handlers (dict): The socket's callbacks, by name.
Returns:
None: This method returns nothing.
"""
self.url = url
self.plan = plan
self.handlers = handlers
self.closed = False
def run_forever(self, **options):
"""Plays the plan and returns when the pretend connection closes.
Args:
**options (dict): The ping and TLS settings the socket asks for.
Returns:
None: This method returns nothing.
"""
print(f'Connecting to {self.url}')
if 'refuse_with' in self.plan:
self.handlers['on_error'](self, HandshakeRefused(self.plan['refuse_with']))
self.handlers['on_close'](self, None, None)
return
self.handlers['on_open'](self)
for frame in self.plan['frames']:
if self.closed:
break
self.handlers['on_message'](self, frame)
self.handlers['on_close'](self, 1000, 'normal closure')
def send(self, data, opcode=None):
"""Prints a message the socket sends to the broker.
Args:
data (str | bytes): The message.
opcode (int | None): The frame type, when the socket names one.
Returns:
None: This method returns nothing.
"""
print(f'Sent: {data}')
def close(self, **options):
"""Notes that the socket asked to close.
Args:
**options (dict): Any close status the socket passes.
Returns:
None: This method returns nothing.
"""
self.closed = True
class ScriptedWebsocketModule:
"""A stand-in for the `websocket` package that hands out scripted connections in turn.
Attributes:
plans (list): What each connection does, in the order the socket connects.
when_finished (callable | None): Called when the last connection is handed out, so the socket stops after it.
"""
def __init__(self, plans):
"""Keeps the connection plans.
Args:
plans (list): What each connection does, in the order the socket connects.
Returns:
None: This method returns nothing.
"""
self.plans = plans
self.when_finished = None
def WebSocketApp(self, url, **handlers):
"""Builds the next scripted connection, closing the socket after the last one.
Args:
url (str): The feed URL.
**handlers (dict): The socket's `on_open`, `on_message`, `on_error` and `on_close` callbacks.
Returns:
ScriptedConnection: The connection.
"""
plan = self.plans.pop(0)
if not self.plans:
self.when_finished()
return ScriptedConnection(url, plan, handlers)
class SubscribingToTheAccountExample:
"""Runs the Shoonya order update socket against one scripted connection.
Attributes:
socket (ShoonyaOrderUpdatesSocket): The socket being shown.
"""
def __init__(self):
"""Builds the frames, the stand-ins, the session and the socket.
Returns:
None: This method returns nothing.
"""
logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
api_module = types.ModuleType('stock_brokers.api.shoonya')
api_module.ShoonyaAPI = StandInShoonyaAPI
sys.modules['stock_brokers.api.shoonya'] = api_module
acknowledgement = {
't': 'ak',
's': 'OK',
}
batch = [
self.order_update('OPEN', '0'),
self.order_update('COMPLETE', '5'),
{
't': 'dk',
'e': 'NSE',
},
]
plans = [
{
'frames': [
json.dumps(acknowledgement),
json.dumps(batch),
],
},
]
websocket_module = ScriptedWebsocketModule(plans)
sys.modules['websocket'] = websocket_module
logger = logging.getLogger('shoonya.orders')
self.socket = ShoonyaOrderUpdatesSocket(ShoonyaSession(logger), self.print_orders, logger)
websocket_module.when_finished = self.socket.close
def order_update(self, status, filled_quantity):
"""One Noren `om` order update for a TCS sell, every value a string.
Args:
status (str): Noren's order status.
filled_quantity (str): How much has filled.
Returns:
dict: The update.
"""
return {
't': 'om',
'norenordno': '26092500012345',
'uid': 'FA12345',
'actid': 'FA12345',
'exch': 'NSE',
'tsym': 'TCS-EQ',
'trantype': 'S',
'qty': '5',
'fillshares': filled_quantity,
'prc': '4120.00',
'prctyp': 'LMT',
'status': status,
}
def print_orders(self, orders, received_at):
"""Prints each order update the socket handed on.
Args:
orders (list): The `om` updates of one frame, exactly as Noren sent them.
received_at (datetime.datetime): When the frame arrived, which is not printed.
Returns:
None: This method returns nothing.
"""
print(f'Callback received {len(orders)} update(s)')
for order in orders:
print(f" Order {order['norenordno']} {order['status']}: {order['trantype']} {order['fillshares']} of {order['qty']} {order['tsym']}")
def run(self):
"""Runs the socket's reconnect loop until the scripted connection ends.
Returns:
None: This method returns nothing.
"""
self.socket.run_forever()
print(f'Gave up: {self.socket.gave_up}')
if __name__ == '__main__':
SubscribingToTheAccountExample().run()
It prints:
Stand-in login number 1
Connecting to wss://api.shoonya.com/NorenWSTP/
Sent: {"t": "a", "uid": "FA12345", "actid": "FA12345", "source": "API", "accesstoken": "shoonya-token-1"}
INFO Order updates socket opened. Authenticating.
Sent: {"t": "o", "actid": "FA12345"}
INFO Authenticated and subscribed. Waiting for order updates.
Callback received 2 update(s)
Order 26092500012345 OPEN: S 0 of 5 TCS-EQ
Order 26092500012345 COMPLETE: S 5 of 5 TCS-EQ
INFO Order updates socket closed with status 1000: normal closure
INFO Order updates socket stopped.
Gave up: False
"""Shows Shoonya's order update socket logging in again, without checking, after Noren rejects its connect frame.
When the `ak` acknowledgement is not `OK`, `ShoonyaOrderUpdatesSocket` closes the connection, and its reconnect loop calls `ShoonyaSession.log_in_again_without_checking`, which logs in whether or not another process already replaced the token.
This program scripts two connections: the first is rejected, the second is accepted and delivers one completed order update. No real socket is opened: a stand-in `websocket` package plays the connections, and a stand-in `stock_brokers.api.shoonya` module counts each `ShoonyaAPI` construction as a login. The backoff is set to zero seconds.
Notice the error line for the rejection, the second login, and the new token in the second connect frame.
Run it from the project root:
python examples/stock_brokers/websockets/shoonya/ShoonyaOrderUpdatesSocket/example_2_rejected_then_logged_in_again.py
"""
import json
import logging
import sys
import types
from stock_brokers.websockets.shoonya import (
ShoonyaOrderUpdatesSocket,
ShoonyaSession,
)
class LoginRecord:
"""The Shoonya login state every stand-in API object reads, standing in for the Redis `last_login` hash.
Attributes:
access_token (str | None): The token in force now.
login_count (int): How many logins the stand-in API has made.
"""
access_token = None
login_count = 0
class StandInShoonyaAPI:
"""A stand-in for `ShoonyaAPI` whose construction is a pretend login."""
def __init__(self):
"""Logs in, which here only issues the next numbered token.
Returns:
None: This method returns nothing.
"""
LoginRecord.login_count = LoginRecord.login_count + 1
LoginRecord.access_token = f'shoonya-token-{LoginRecord.login_count}'
print(f'Stand-in login number {LoginRecord.login_count}')
self._settings = {
'ucc_code': 'FA12345',
}
def _current_login(self):
"""The login in force now, as the real class reads it from Redis.
Returns:
dict | None: The access token, or None before any login.
"""
if LoginRecord.access_token is None:
return None
return {
'access_token': LoginRecord.access_token,
}
class HandshakeRefused(Exception):
"""A refused websocket handshake, carrying the HTTP status the way `websocket-client` does.
Attributes:
status_code (int): The HTTP status the broker answered with.
"""
def __init__(self, status_code):
"""Keeps the status.
Args:
status_code (int): The HTTP status the broker answered with.
Returns:
None: This method returns nothing.
"""
super().__init__(f'Handshake status {status_code}')
self.status_code = status_code
class ScriptedConnection:
"""A stand-in for `websocket.WebSocketApp` that either refuses the handshake or plays frames to the socket's handlers.
Attributes:
url (str): The URL the socket asked for.
plan (dict): Either a `refuse_with` status or a list of `frames`.
handlers (dict): The socket's callbacks, by name.
closed (bool): Whether the socket asked to close, after which no more frames are delivered.
"""
def __init__(self, url, plan, handlers):
"""Keeps what the socket passed in.
Args:
url (str): The URL the socket asked for.
plan (dict): Either a `refuse_with` status or a list of `frames`.
handlers (dict): The socket's callbacks, by name.
Returns:
None: This method returns nothing.
"""
self.url = url
self.plan = plan
self.handlers = handlers
self.closed = False
def run_forever(self, **options):
"""Plays the plan and returns when the pretend connection closes.
Args:
**options (dict): The ping and TLS settings the socket asks for.
Returns:
None: This method returns nothing.
"""
print(f'Connecting to {self.url}')
if 'refuse_with' in self.plan:
self.handlers['on_error'](self, HandshakeRefused(self.plan['refuse_with']))
self.handlers['on_close'](self, None, None)
return
self.handlers['on_open'](self)
for frame in self.plan['frames']:
if self.closed:
break
self.handlers['on_message'](self, frame)
self.handlers['on_close'](self, 1000, 'normal closure')
def send(self, data, opcode=None):
"""Prints a message the socket sends to the broker.
Args:
data (str | bytes): The message.
opcode (int | None): The frame type, when the socket names one.
Returns:
None: This method returns nothing.
"""
print(f'Sent: {data}')
def close(self, **options):
"""Notes that the socket asked to close.
Args:
**options (dict): Any close status the socket passes.
Returns:
None: This method returns nothing.
"""
self.closed = True
class ScriptedWebsocketModule:
"""A stand-in for the `websocket` package that hands out scripted connections in turn.
Attributes:
plans (list): What each connection does, in the order the socket connects.
when_finished (callable | None): Called when the last connection is handed out, so the socket stops after it.
"""
def __init__(self, plans):
"""Keeps the connection plans.
Args:
plans (list): What each connection does, in the order the socket connects.
Returns:
None: This method returns nothing.
"""
self.plans = plans
self.when_finished = None
def WebSocketApp(self, url, **handlers):
"""Builds the next scripted connection, closing the socket after the last one.
Args:
url (str): The feed URL.
**handlers (dict): The socket's `on_open`, `on_message`, `on_error` and `on_close` callbacks.
Returns:
ScriptedConnection: The connection.
"""
plan = self.plans.pop(0)
if not self.plans:
self.when_finished()
return ScriptedConnection(url, plan, handlers)
class RejectedThenLoggedInAgainExample:
"""Runs the Shoonya order update socket through a rejection and a good connection.
Attributes:
socket (ShoonyaOrderUpdatesSocket): The socket being shown.
"""
def __init__(self):
"""Builds the frames, the stand-ins, the session and the socket.
Returns:
None: This method returns nothing.
"""
logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
api_module = types.ModuleType('stock_brokers.api.shoonya')
api_module.ShoonyaAPI = StandInShoonyaAPI
sys.modules['stock_brokers.api.shoonya'] = api_module
rejected = {
't': 'ak',
's': 'NOT_OK',
}
accepted = {
't': 'ak',
's': 'OK',
}
plans = [
{
'frames': [
json.dumps(rejected),
],
},
{
'frames': [
json.dumps(accepted),
json.dumps(self.order_update('COMPLETE', '5')),
],
},
]
websocket_module = ScriptedWebsocketModule(plans)
sys.modules['websocket'] = websocket_module
logger = logging.getLogger('shoonya.orders')
self.socket = ShoonyaOrderUpdatesSocket(ShoonyaSession(logger), self.print_orders, logger)
self.socket.MIN_BACKOFF_SECONDS = 0
websocket_module.when_finished = self.socket.close
def order_update(self, status, filled_quantity):
"""One Noren `om` order update for a TCS sell, every value a string.
Args:
status (str): Noren's order status.
filled_quantity (str): How much has filled.
Returns:
dict: The update.
"""
return {
't': 'om',
'norenordno': '26092500012345',
'uid': 'FA12345',
'actid': 'FA12345',
'exch': 'NSE',
'tsym': 'TCS-EQ',
'trantype': 'S',
'qty': '5',
'fillshares': filled_quantity,
'prc': '4120.00',
'prctyp': 'LMT',
'status': status,
}
def print_orders(self, orders, received_at):
"""Prints each order update the socket handed on.
Args:
orders (list): The `om` updates of one frame, exactly as Noren sent them.
received_at (datetime.datetime): When the frame arrived, which is not printed.
Returns:
None: This method returns nothing.
"""
print(f'Callback received {len(orders)} update(s)')
for order in orders:
print(f" Order {order['norenordno']} {order['status']}: {order['trantype']} {order['fillshares']} of {order['qty']} {order['tsym']}")
def run(self):
"""Runs the socket's reconnect loop until the scripted connections run out.
Returns:
None: This method returns nothing.
"""
self.socket.run_forever()
print(f'Gave up: {self.socket.gave_up}')
print(f'Logins made: {LoginRecord.login_count}')
if __name__ == '__main__':
RejectedThenLoggedInAgainExample().run()
It prints:
Stand-in login number 1
Connecting to wss://api.shoonya.com/NorenWSTP/
Sent: {"t": "a", "uid": "FA12345", "actid": "FA12345", "source": "API", "accesstoken": "shoonya-token-1"}
INFO Order updates socket opened. Authenticating.
ERROR Authentication refused: {'t': 'ak', 's': 'NOT_OK'}
INFO Order updates socket closed with status 1000: normal closure
WARNING Logging in to Shoonya again.
Stand-in login number 2
WARNING Order updates socket disconnected. Reconnecting in 0 seconds.
Connecting to wss://api.shoonya.com/NorenWSTP/
Sent: {"t": "a", "uid": "FA12345", "actid": "FA12345", "source": "API", "accesstoken": "shoonya-token-2"}
INFO Order updates socket opened. Authenticating.
Sent: {"t": "o", "actid": "FA12345"}
INFO Authenticated and subscribed. Waiting for order updates.
Callback received 1 update(s)
Order 26092500012345 COMPLETE: S 5 of 5 TCS-EQ
INFO Order updates socket closed with status 1000: normal closure
INFO Order updates socket stopped.
Gave up: False
Logins made: 2
Methods:
| Name | Description |
|---|---|
close |
Closes the connection and stops reconnecting. |
run_forever |
Connects, and reconnects with backoff, until closed or unable to recover. |
Source code in stock_brokers/websockets/shoonya.py
_connect
¶
Opens the websocket and blocks until it closes, sending Noren's heartbeat meanwhile.
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Source code in stock_brokers/websockets/shoonya.py
_log_in_again
¶
Logs in again, without checking whether another process already replaced the token.
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
_on_open
¶
Authenticates with the token in force now; the subscription waits for the acknowledgement.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
websocket_connection
|
WebSocketApp
|
The connection that opened. |
required |
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Source code in stock_brokers/websockets/shoonya.py
_on_error
¶
Reports an error.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
websocket_connection
|
WebSocketApp
|
The connection that failed. |
required |
error
|
Exception
|
The error. |
required |
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Source code in stock_brokers/websockets/shoonya.py
_on_message
¶
Subscribes on the connect acknowledgement, and hands each message's order updates on.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
websocket_connection
|
WebSocketApp
|
The connection the message arrived on. |
required |
message
|
bytes | str
|
The frame: one JSON message or a JSON list of them. |
required |
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Source code in stock_brokers/websockets/shoonya.py
close
¶
Closes the connection and stops reconnecting.
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Example usage
run_forever
¶
Connects, and reconnects with backoff, until closed or unable to recover.
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Example usage
Source code in stock_brokers/websockets/base.py
_gives_up_after_logging_in_again
¶
Whether a socket that has already logged in again gives up on this failure.
The loop asks only when the connection was refused or has failed six times in a row. By default the answer is yes, so a refusal straight after logging in again ends the socket. A broker whose refusals are not always about the login overrides this to give up only after six more failed connects.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
failed_connects
|
int
|
How many connects in a row have failed since the last login. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True to give up now. |
Source code in stock_brokers/websockets/base.py
_on_close
¶
Reports that the connection closed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
websocket_connection
|
WebSocketApp
|
The connection that closed. |
required |
status_code
|
int | None
|
The close status the broker sent. |
required |
message
|
str | None
|
The close reason the broker sent. |
required |
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |