indmoney
¶
INDmoney's two websockets: the INDstocks price feed and the account's order updates.
The login.
IndmoneySession holds the INDMoneyAPI both sockets read their credentials from, afresh on every connect, so a login made by any process is picked up.
The token travels in the handshake as Authorization, with the client id as x-api-key when there is one, and INDstocks refuses a dead token at the handshake itself with HTTP 401, 403 or 513.
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.
Quotes.
IndmoneyQuotesSocket connects to wss://ws-prices.indstocks.com/api/v1/ws/prices and subscribes its batch in full mode, the richest the feed serves: ltp carries only the last price, quote carries the day's open, high, low, close and volume without a last price, and depth, market_depth, depth5 and snapquote send nothing.
Every update arrives as a JSON string whose content is itself JSON, followed by a newline, so each line of a frame is decoded twice.
An update names the instrument by its bare security id, a millisecond timestamp, and a data object carrying only what changed, so each instrument's fields are merged across updates.
Every socket carries instruments of one segment only, because only the socket's segment places a bare security id.
A message without data is logged rather than silently dropped, and no tick is handed on until an instrument has a last price.
close is sent as the last price, not the previous close, and the best bid and offer are the only depth, without quantities.
Order updates.
IndmoneyOrderUpdatesSocket connects to wss://ws-order-updates.indstocks.com/api/v1/ws/trades and asks for order_update messages.
Subscription acknowledgements and heartbeats share the connection; an order arrives as {"type": "order", "data": {...}}, or with the order's fields beside type when there is no data object.
Neither socket writes Redis.
The quotes socket hands each frame's ticks to on_ticks, and the order socket hands each message's orders to on_updates; the scripts in bin/indmoney/ do the writing.
Classes:
| Name | Description |
|---|---|
IndmoneySession |
The INDmoney login every socket authenticates with, logged in again by one socket at a time. |
IndmoneyQuotesSocket |
One INDstocks price feed websocket carrying the full quotes of a batch of instruments of one segment. |
IndmoneyOrderUpdatesSocket |
INDstocks' order update websocket, authenticated by its handshake. |
IndmoneySession
¶
The INDmoney login every socket authenticates with, logged in again by one socket at a time.
Constructs INDMoneyAPI, 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 the handshake headers an INDmoney socket presents and logs in again only when the failed token is still current.
`IndmoneySession` holds one `INDMoneyAPI` and reads the login afresh whenever a socket connects. `credentials` returns the client id and the token in force, and `handshake_headers` turns them into the headers INDstocks expects in the websocket handshake: the token as `Authorization` and the client id as `x-api-key`. When a quotes socket is refused, `log_in_again` logs in only if the token it used is still the one in force.
Constructing the real `INDMoneyAPI` logs in to INDmoney, so the program registers a stand-in module under `stock_brokers.api.indmoney`. Each construction of the stand-in counts as one login and issues the next numbered token, and `_current_login` answers from a shared record standing in for the Redis `last_login` hash.
Notice that the headers change after the login, and that the second request with the stale token makes no login.
Run it from the project root:
python examples/stock_brokers/websockets/indmoney/IndmoneySession/example_1_handshake_headers.py
"""
import logging
import sys
import types
from stock_brokers.websockets.indmoney import (
IndmoneySession,
)
class LoginRecord:
"""The INDmoney 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 StandInINDMoneyAPI:
"""A stand-in for `INDMoneyAPI` 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'indmoney-token-{LoginRecord.login_count}'
print(f'Stand-in login number {LoginRecord.login_count}')
self._settings = {
'client_id': 'ind-client-7',
}
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 HandshakeHeadersExample:
"""Prints an INDmoney session's credentials and handshake headers around a login.
Attributes:
session (IndmoneySession): 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.indmoney')
api_module.INDMoneyAPI = StandInINDMoneyAPI
sys.modules['stock_brokers.api.indmoney'] = api_module
self.session = IndmoneySession(logging.getLogger('indmoney.session'))
def run(self):
"""Prints the headers, logs in again twice with the same stale token, and prints them again.
Returns:
None: This method returns nothing.
"""
client_id, failed_token = self.session.credentials()
print(f'Credentials: client_id={client_id} access_token={failed_token}')
headers, token = self.session.handshake_headers()
print(f'Handshake headers: {headers} for token {token}')
self.session.log_in_again(failed_token)
self.session.log_in_again(failed_token)
headers, token = self.session.handshake_headers()
print(f'Handshake headers: {headers} for token {token}')
print(f'Logins made: {LoginRecord.login_count}')
if __name__ == '__main__':
HandshakeHeadersExample().run()
It prints:
Stand-in login number 1
Credentials: client_id=ind-client-7 access_token=indmoney-token-1
Handshake headers: {'Authorization': 'indmoney-token-1', 'x-api-key': 'ind-client-7'} for token indmoney-token-1
WARNING Logging in to INDmoney again.
Stand-in login number 2
Handshake headers: {'Authorization': 'indmoney-token-2', 'x-api-key': 'ind-client-7'} for token indmoney-token-2
Logins made: 2
"""Shows the handshake headers without a client id, and a login made without checking the token.
An INDmoney account whose settings have no client id presents only the `Authorization` header, because `handshake_headers` adds `x-api-key` only when there is a client id. The order update socket logs in again with `log_in_again_without_checking`, which logs in even when another process already replaced the token that failed.
Constructing the real `INDMoneyAPI` logs in to INDmoney, so the program registers a stand-in module under `stock_brokers.api.indmoney` whose settings hold no client id and whose constructions count as logins. The program replaces the token as another process would before logging in.
Notice that the headers hold `Authorization` alone, and that the unconditional login replaces the other process's token.
Run it from the project root:
python examples/stock_brokers/websockets/indmoney/IndmoneySession/example_2_no_client_id_and_unchecked_login.py
"""
import logging
import sys
import types
from stock_brokers.websockets.indmoney import (
IndmoneySession,
)
class LoginRecord:
"""The INDmoney 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 StandInINDMoneyAPI:
"""A stand-in for `INDMoneyAPI` 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'indmoney-token-{LoginRecord.login_count}'
print(f'Stand-in login number {LoginRecord.login_count}')
self._settings = {
'client_id': None,
}
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 NoClientIdAndUncheckedLoginExample:
"""Prints headers without a client id and logs in again unconditionally.
Attributes:
session (IndmoneySession): 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.indmoney')
api_module.INDMoneyAPI = StandInINDMoneyAPI
sys.modules['stock_brokers.api.indmoney'] = api_module
self.session = IndmoneySession(logging.getLogger('indmoney.session'))
def run(self):
"""Prints the headers, replaces the token elsewhere, and logs in without checking.
Returns:
None: This method returns nothing.
"""
headers, token = self.session.handshake_headers()
print(f'Handshake headers: {headers}')
LoginRecord.access_token = 'indmoney-token-from-another-process'
print(f'Another process replaced {token}.')
self.session.log_in_again_without_checking()
print(f'Credentials now: {self.session.credentials()}')
print(f'Logins made: {LoginRecord.login_count}')
if __name__ == '__main__':
NoClientIdAndUncheckedLoginExample().run()
It prints:
Methods:
| Name | Description |
|---|---|
credentials |
The api key (client id) and the access token in force now, which may be one another process has just obtained. |
handshake_headers |
The headers that present the credentials in force now in the websocket handshake. |
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/indmoney.py
credentials
¶
The api key (client id) and the access token in force now, which may be one another process has just obtained.
Returns:
| Type | Description |
|---|---|
tuple
|
The client id and the access token, either of which may be None. |
Example usage
print(f'Credentials now: {self.session.credentials()}')
Source code in stock_brokers/websockets/indmoney.py
handshake_headers
¶
The headers that present the credentials in force now in the websocket handshake.
Returns:
| Type | Description |
|---|---|
tuple
|
The headers as a dictionary, and the access token they carry. |
Example usage
Source code in stock_brokers/websockets/indmoney.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 access 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/indmoney.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/indmoney.py
_log_in
¶
Logs in by constructing INDMoneyAPI 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/indmoney.py
IndmoneyQuotesSocket
¶
Bases: BrokerWebsocket
One INDstocks price feed websocket carrying the full quotes of a batch of instruments of one segment.
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, which becomes a tick's |
required |
session
|
IndmoneySession
|
The shared login. |
required |
on_ticks
|
callable
|
Called with each frame's list of ticks, on this socket's thread. |
required |
logger
|
Logger
|
Where the connection reports. |
required |
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Example programs
"""Decodes INDstocks price feed updates, which are JSON inside JSON, and merges them into ticks.
`IndmoneyQuotesSocket` presents the token in the websocket handshake and subscribes its batch in `full` mode. Every update arrives as a JSON string whose content is itself JSON, one per line, so each line is decoded twice. An update names the instrument by its bare security id and carries only the fields that changed, so the socket maps the id back to the subscribed `SEGMENT:TOKEN` and merges each instrument's fields across updates. No tick is handed on until an instrument has a last price.
This program scripts one connection whose single frame holds two lines for NSE security 2885: the first carries the day's open, high, low, close and volume without a last price, and the second carries the last price, the best bid and offer and a millisecond timestamp. No real socket is opened: a stand-in `websocket` package plays the frame and prints the handshake headers and what the socket sends, and a stand-in `stock_brokers.api.indmoney` module lets the real `IndmoneySession` supply credentials without logging in.
Notice that only one tick comes out, that it holds the fields of both lines, that the millisecond timestamp becomes epoch seconds, and that the depth is the best bid and offer only, without quantities.
Run it from the project root:
python examples/stock_brokers/websockets/indmoney/IndmoneyQuotesSocket/example_1_merging_double_encoded_updates.py
"""
import json
import logging
import sys
import types
from stock_brokers.websockets.indmoney import (
IndmoneyQuotesSocket,
IndmoneySession,
)
class LoginRecord:
"""The INDmoney 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 StandInINDMoneyAPI:
"""A stand-in for `INDMoneyAPI` 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'indmoney-token-{LoginRecord.login_count}'
print(f'Stand-in login number {LoginRecord.login_count}')
self._settings = {
'client_id': 'ind-client-7',
}
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.
"""
print(f"Handshake headers: {handlers.get('header')}")
plan = self.plans.pop(0)
if not self.plans:
self.when_finished()
return ScriptedConnection(url, plan, handlers)
class MergingDoubleEncodedUpdatesExample:
"""Runs an INDmoney quotes socket against one scripted frame of two lines.
Attributes:
socket (IndmoneyQuotesSocket): The socket being shown.
"""
def __init__(self):
"""Builds the frame, 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.indmoney')
api_module.INDMoneyAPI = StandInINDMoneyAPI
sys.modules['stock_brokers.api.indmoney'] = api_module
day_fields = {
'instrument': '2885',
'data': {
'open': 2930.0,
'high': 2960.0,
'low': 2925.0,
'close': 2950.0,
'volume': 1204500.0,
},
}
price_fields = {
'instrument': '2885',
'timestamp': 1790311529000,
'data': {
'ltp': 2950.5,
'ltt': 1790311528,
'bid_price': 2950.0,
'ask_price': 2950.5,
'total_buy_qty': 54000,
'total_sell_qty': 61000,
},
}
frame = self.feed_line(day_fields) + self.feed_line(price_fields)
plans = [
{
'frames': [
frame,
],
},
]
websocket_module = ScriptedWebsocketModule(plans)
sys.modules['websocket'] = websocket_module
logger = logging.getLogger('indmoney.quotes')
tokens = [
'NSE:2885',
]
names = {
'NSE:2885': 'NSE:RELIANCE',
}
self.socket = IndmoneyQuotesSocket('socket_0', tokens, names, IndmoneySession(logger), self.print_ticks, logger)
websocket_module.when_finished = self.socket.close
def feed_line(self, update):
"""One line as INDstocks sends it: a JSON string whose content is itself JSON, then a newline.
Args:
update (dict): The update.
Returns:
str: The line.
"""
return json.dumps(json.dumps(update)) + '\n'
def print_ticks(self, ticks):
"""Prints the main fields of each tick the socket built.
Args:
ticks (list): The normalized ticks from one frame.
Returns:
None: This method returns nothing.
"""
for tick in ticks:
print(f"Tick {tick['id']} ({tick['instrument_token']}) on {tick['exchange']}")
print(f" last_price={tick['last_price']} volume={tick['volume']} buy_quantity={tick['buy_quantity']} sell_quantity={tick['sell_quantity']}")
print(f" ohlc={tick['ohlc']}")
print(f" last_trade_time={tick['last_trade_time']} exchange_timestamp={tick['exchange_timestamp']}")
print(f" depth={tick['depth']}")
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__':
MergingDoubleEncodedUpdatesExample().run()
It prints:
Stand-in login number 1
Handshake headers: {'Authorization': 'indmoney-token-1', 'x-api-key': 'ind-client-7'}
Connecting to wss://ws-prices.indstocks.com/api/v1/ws/prices
Sent: {"action": "subscribe", "mode": "full", "instruments": ["NSE:2885"]}
INFO socket_0 opened and subscribed to 1 instrument(s) in full mode.
Tick NSE:RELIANCE (NSE:2885) on NSE
last_price=2950.5 volume=1204500 buy_quantity=54000 sell_quantity=61000
ohlc={'open': 2930.0, 'high': 2960.0, 'low': 2925.0, 'close': 2950.0}
last_trade_time=1790311528 exchange_timestamp=1790311529
depth={'buy': [{'quantity': None, 'price': 2950.0, 'orders': None}], 'sell': [{'quantity': None, 'price': 2950.5, 'orders': None}]}
INFO socket_0 closed with status 1000: normal closure
INFO socket_0 stopped.
Gave up: False
"""Shows an INDmoney quotes socket logging in again after INDstocks refuses its handshake with HTTP 513.
INDstocks refuses a dead token at the websocket handshake with HTTP 401, 403 or 513. `IndmoneyQuotesSocket` takes any of those as a dead session, and its reconnect loop asks the shared `IndmoneySession` to log in again, skipping the login if another socket or process already replaced the token, before connecting with new handshake headers.
This program scripts two connections. The first is refused with 513. The second delivers a message without instrument data, which the socket logs rather than drops, and then one BSE update carrying a last price. No real socket is opened: a stand-in `websocket` package plays the connections and prints each handshake's headers, and a stand-in `stock_brokers.api.indmoney` module counts each `INDMoneyAPI` construction as one login. The backoff is set to zero seconds.
Notice that the second handshake carries the new token in `Authorization`, and that the logged message is the subscription acknowledgement INDstocks sends without `data`.
Run it from the project root:
python examples/stock_brokers/websockets/indmoney/IndmoneyQuotesSocket/example_2_refused_handshake.py
"""
import json
import logging
import sys
import types
from stock_brokers.websockets.indmoney import (
IndmoneyQuotesSocket,
IndmoneySession,
)
class LoginRecord:
"""The INDmoney 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 StandInINDMoneyAPI:
"""A stand-in for `INDMoneyAPI` 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'indmoney-token-{LoginRecord.login_count}'
print(f'Stand-in login number {LoginRecord.login_count}')
self._settings = {
'client_id': 'ind-client-7',
}
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.
"""
print(f"Handshake headers: {handlers.get('header')}")
plan = self.plans.pop(0)
if not self.plans:
self.when_finished()
return ScriptedConnection(url, plan, handlers)
class RefusedHandshakeExample:
"""Runs an INDmoney quotes socket through a refused handshake and a good connection.
Attributes:
socket (IndmoneyQuotesSocket): 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.indmoney')
api_module.INDMoneyAPI = StandInINDMoneyAPI
sys.modules['stock_brokers.api.indmoney'] = api_module
acknowledgement = {
'status': 'subscribed',
'mode': 'full',
}
update = {
'instrument': '500325',
'timestamp': 1790311529000,
'data': {
'ltp': 2951.0,
'volume': 88000,
},
}
plans = [
{
'refuse_with': 513,
},
{
'frames': [
self.feed_line(acknowledgement),
self.feed_line(update),
],
},
]
websocket_module = ScriptedWebsocketModule(plans)
sys.modules['websocket'] = websocket_module
logger = logging.getLogger('indmoney.quotes')
tokens = [
'BSE:500325',
]
names = {
'BSE:500325': 'BSE:RELIANCE',
}
self.socket = IndmoneyQuotesSocket('socket_0', tokens, names, IndmoneySession(logger), self.print_ticks, logger)
self.socket.MIN_BACKOFF_SECONDS = 0
websocket_module.when_finished = self.socket.close
def feed_line(self, update):
"""One line as INDstocks sends it: a JSON string whose content is itself JSON, then a newline.
Args:
update (dict): The update.
Returns:
str: The line.
"""
return json.dumps(json.dumps(update)) + '\n'
def print_ticks(self, ticks):
"""Prints the main fields of each tick the socket built.
Args:
ticks (list): The normalized ticks from one frame.
Returns:
None: This method returns nothing.
"""
for tick in ticks:
print(f"Tick {tick['id']} ({tick['instrument_token']}) on {tick['exchange']}")
print(f" last_price={tick['last_price']} volume={tick['volume']} buy_quantity={tick['buy_quantity']} sell_quantity={tick['sell_quantity']}")
print(f" ohlc={tick['ohlc']}")
print(f" last_trade_time={tick['last_trade_time']} exchange_timestamp={tick['exchange_timestamp']}")
print(f" depth={tick['depth']}")
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__':
RefusedHandshakeExample().run()
It prints:
Stand-in login number 1
Handshake headers: {'Authorization': 'indmoney-token-1', 'x-api-key': 'ind-client-7'}
Connecting to wss://ws-prices.indstocks.com/api/v1/ws/prices
WARNING socket_0 handshake refused with HTTP 513, taken as a dead session.
INFO socket_0 closed with status None: None
WARNING Logging in to INDmoney again.
Stand-in login number 2
WARNING socket_0 disconnected. Reconnecting in 0 seconds.
Handshake headers: {'Authorization': 'indmoney-token-2', 'x-api-key': 'ind-client-7'}
Connecting to wss://ws-prices.indstocks.com/api/v1/ws/prices
Sent: {"action": "subscribe", "mode": "full", "instruments": ["BSE:500325"]}
INFO socket_0 opened and subscribed to 1 instrument(s) in full mode.
INFO socket_0 message without instrument data: {"status": "subscribed", "mode": "full"}
Tick BSE:RELIANCE (BSE:500325) on BSE
last_price=2951.0 volume=88000 buy_quantity=None sell_quantity=None
ohlc={'open': None, 'high': None, 'low': None, 'close': None}
last_trade_time=None exchange_timestamp=1790311529
depth={'buy': [], 'sell': []}
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/indmoney.py
_connect
¶
Opens the websocket with the token in force now, presented in the handshake, and blocks until it closes.
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Source code in stock_brokers/websockets/indmoney.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. |
Source code in stock_brokers/websockets/indmoney.py
_on_open
¶
Subscribes the batch in full mode.
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/indmoney.py
_on_error
¶
Takes a refused handshake as a dead session, and reports any other error.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
websocket_connection
|
WebSocketApp
|
The connection that failed. |
required |
error
|
Exception
|
The error, which carries |
required |
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Source code in stock_brokers/websockets/indmoney.py
_on_message
¶
Decodes a frame into ticks and hands them on.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
websocket_connection
|
WebSocketApp
|
The connection the frame arrived on. |
required |
message
|
bytes | str
|
The frame. |
required |
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Source code in stock_brokers/websockets/indmoney.py
_decode_entries
¶
Decodes one frame into its update dictionaries, decoding a line a second time when it holds a JSON string.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
bytes | str
|
The frame. |
required |
Returns:
| Type | Description |
|---|---|
list[dict]
|
The decoded updates, in order. |
Source code in stock_brokers/websockets/indmoney.py
_parse_ticks
¶
Builds the ticks in one frame, merging each instrument's fields across updates.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
bytes | str
|
The frame. |
required |
Returns:
| Type | Description |
|---|---|
list[dict]
|
One tick per update that left its instrument with a last price. |
Source code in stock_brokers/websockets/indmoney.py
_subscribed_token
¶
The subscribed SEGMENT:TOKEN for a bare security id; a socket carries one segment, so at most one matches.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bare
|
str
|
The security id as the feed sent it. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The subscribed token, or the bare id when this socket did not subscribe it. |
Source code in stock_brokers/websockets/indmoney.py
_epoch_seconds
¶
A feed time as whole epoch seconds.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
int | float | str | None
|
The time as sent, in milliseconds, or in seconds for a small value. |
required |
Returns:
| Type | Description |
|---|---|
int | None
|
Epoch seconds, or None for a missing or zero time. |
Source code in stock_brokers/websockets/indmoney.py
_build_tick
¶
Builds a normalized tick from an instrument's merged full mode fields.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token
|
str
|
The instrument's |
required |
state
|
dict
|
Its fields merged across updates, with the latest update's |
required |
Returns:
| Type | Description |
|---|---|
dict
|
The tick. |
Source code in stock_brokers/websockets/indmoney.py
_number
¶
A feed value as a float.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
object
|
The raw value. |
required |
Returns:
| Type | Description |
|---|---|
float | None
|
The number, or None for a blank, null or unreadable value. |
Source code in stock_brokers/websockets/indmoney.py
_whole
¶
A feed quantity as an integer; quantities are counts, so a float such as 12.0 becomes 12.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
object
|
The raw value. |
required |
Returns:
| Type | Description |
|---|---|
int | None
|
The count, or None. |
Source code in stock_brokers/websockets/indmoney.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
IndmoneyOrderUpdatesSocket
¶
Bases: BrokerWebsocket
INDstocks' order update websocket, authenticated by its handshake.
Sets up the order updates socket.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
IndmoneySession
|
The login. |
required |
on_updates
|
callable
|
Called on this socket's thread with each message's orders, as dictionaries exactly as INDstocks sent them, and the |
required |
logger
|
Logger
|
Where the connection reports. |
required |
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Example programs
"""Receives INDstocks order updates in both of the shapes the feed uses and prints the orders handed on.
`IndmoneyOrderUpdatesSocket` is authenticated by its handshake headers and asks for `order_update` messages once it opens. The subscription acknowledgement and heartbeats share the connection with the orders. An order arrives either as `{"type": "order", "data": {...}}` or with the order's fields beside `type` when there is no `data` object, and the socket hands on the order in both cases.
This program scripts one connection delivering the acknowledgement, a heartbeat, an order wrapped in `data`, and an order with its fields at the top level. No real socket is opened: a stand-in `websocket` package plays the frames and prints the handshake headers and what the socket sends, and a stand-in `stock_brokers.api.indmoney` module lets the real `IndmoneySession` supply credentials without logging in.
Notice the acknowledgement line, the ignored heartbeat, and that the flat order reaches the callback with its `type` field still in it, because it is handed on exactly as sent.
Run it from the project root:
python examples/stock_brokers/websockets/indmoney/IndmoneyOrderUpdatesSocket/example_1_orders_in_both_shapes.py
"""
import json
import logging
import sys
import types
from stock_brokers.websockets.indmoney import (
IndmoneyOrderUpdatesSocket,
IndmoneySession,
)
class LoginRecord:
"""The INDmoney 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 StandInINDMoneyAPI:
"""A stand-in for `INDMoneyAPI` 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'indmoney-token-{LoginRecord.login_count}'
print(f'Stand-in login number {LoginRecord.login_count}')
self._settings = {
'client_id': 'ind-client-7',
}
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.
"""
print(f"Handshake headers: {handlers.get('header')}")
plan = self.plans.pop(0)
if not self.plans:
self.when_finished()
return ScriptedConnection(url, plan, handlers)
class OrdersInBothShapesExample:
"""Runs the INDmoney order update socket against one scripted connection.
Attributes:
socket (IndmoneyOrderUpdatesSocket): The socket being shown.
"""
def __init__(self):
"""Builds the messages, 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.indmoney')
api_module.INDMoneyAPI = StandInINDMoneyAPI
sys.modules['stock_brokers.api.indmoney'] = api_module
acknowledgement = {
'action': 'subscribe',
'status': 'success',
}
heartbeat = {
'type': 'heartbeat',
}
wrapped = {
'type': 'order',
'data': self.order('OPEN', 0),
}
flat = self.order('COMPLETE', 5)
flat['type'] = 'order'
plans = [
{
'frames': [
json.dumps(acknowledgement),
json.dumps(heartbeat),
json.dumps(wrapped),
json.dumps(flat),
],
},
]
websocket_module = ScriptedWebsocketModule(plans)
sys.modules['websocket'] = websocket_module
logger = logging.getLogger('indmoney.orders')
self.socket = IndmoneyOrderUpdatesSocket(IndmoneySession(logger), self.print_orders, logger)
websocket_module.when_finished = self.socket.close
def order(self, status, filled_quantity):
"""An INDstocks order for a TCS sell.
Args:
status (str): The order status.
filled_quantity (int): How much has filled.
Returns:
dict: The order.
"""
return {
'order_id': 'IND2609250001',
'order_status': status,
'trading_symbol': 'TCS',
'exchange': 'NSE',
'txn_type': 'SELL',
'qty': 5,
'filled_qty': filled_quantity,
'price': 4120.0,
}
def print_orders(self, orders, received_at):
"""Prints each order the socket handed on.
Args:
orders (list): The orders of one message, exactly as INDstocks sent them.
received_at (datetime.datetime): When the message arrived, which is not printed.
Returns:
None: This method returns nothing.
"""
for order in orders:
print(f"Order type={order.get('type')} {order['order_id']} {order['order_status']}: {order['txn_type']} {order['filled_qty']} of {order['qty']} {order['trading_symbol']}")
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__':
OrdersInBothShapesExample().run()
It prints:
Stand-in login number 1
Handshake headers: {'Authorization': 'indmoney-token-1', 'x-api-key': 'ind-client-7'}
Connecting to wss://ws-order-updates.indstocks.com/api/v1/ws/trades
Sent: {"action": "subscribe", "mode": "order_update"}
INFO Order updates socket opened and subscribed. Waiting for order updates.
INFO Order subscription acknowledged.
INFO Ignoring a message of type 'heartbeat': {"type": "heartbeat"}
Order type=None IND2609250001 OPEN: SELL 0 of 5 TCS
Order type=order IND2609250001 COMPLETE: SELL 5 of 5 TCS
INFO Order updates socket closed with status 1000: normal closure
INFO Order updates socket stopped.
Gave up: False
"""Shows INDmoney's order update socket logging in again, without checking, after a refused handshake.
When INDstocks refuses the order socket's handshake with HTTP 401, 403 or 513, `IndmoneyOrderUpdatesSocket` treats it as a dead session and its reconnect loop calls `IndmoneySession.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 refused with HTTP 401, and the second opens and delivers one completed order. No real socket is opened: a stand-in `websocket` package plays the connections and prints each handshake's headers, and a stand-in `stock_brokers.api.indmoney` module counts each `INDMoneyAPI` construction as one login. The backoff is set to zero seconds.
Notice the warning about the refused handshake, the second login, and the new token in the second handshake.
Run it from the project root:
python examples/stock_brokers/websockets/indmoney/IndmoneyOrderUpdatesSocket/example_2_refused_then_logged_in_again.py
"""
import json
import logging
import sys
import types
from stock_brokers.websockets.indmoney import (
IndmoneyOrderUpdatesSocket,
IndmoneySession,
)
class LoginRecord:
"""The INDmoney 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 StandInINDMoneyAPI:
"""A stand-in for `INDMoneyAPI` 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'indmoney-token-{LoginRecord.login_count}'
print(f'Stand-in login number {LoginRecord.login_count}')
self._settings = {
'client_id': 'ind-client-7',
}
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.
"""
print(f"Handshake headers: {handlers.get('header')}")
plan = self.plans.pop(0)
if not self.plans:
self.when_finished()
return ScriptedConnection(url, plan, handlers)
class RefusedThenLoggedInAgainExample:
"""Runs the INDmoney order update socket through a refused handshake and a good connection.
Attributes:
socket (IndmoneyOrderUpdatesSocket): The socket being shown.
"""
def __init__(self):
"""Builds the messages, 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.indmoney')
api_module.INDMoneyAPI = StandInINDMoneyAPI
sys.modules['stock_brokers.api.indmoney'] = api_module
update = {
'type': 'order',
'data': self.order('COMPLETE', 5),
}
plans = [
{
'refuse_with': 401,
},
{
'frames': [
json.dumps(update),
],
},
]
websocket_module = ScriptedWebsocketModule(plans)
sys.modules['websocket'] = websocket_module
logger = logging.getLogger('indmoney.orders')
self.socket = IndmoneyOrderUpdatesSocket(IndmoneySession(logger), self.print_orders, logger)
self.socket.MIN_BACKOFF_SECONDS = 0
websocket_module.when_finished = self.socket.close
def order(self, status, filled_quantity):
"""An INDstocks order for a TCS sell.
Args:
status (str): The order status.
filled_quantity (int): How much has filled.
Returns:
dict: The order.
"""
return {
'order_id': 'IND2609250001',
'order_status': status,
'trading_symbol': 'TCS',
'exchange': 'NSE',
'txn_type': 'SELL',
'qty': 5,
'filled_qty': filled_quantity,
'price': 4120.0,
}
def print_orders(self, orders, received_at):
"""Prints each order the socket handed on.
Args:
orders (list): The orders of one message, exactly as INDstocks sent them.
received_at (datetime.datetime): When the message arrived, which is not printed.
Returns:
None: This method returns nothing.
"""
for order in orders:
print(f"Order {order['order_id']} {order['order_status']}: {order['txn_type']} {order['filled_qty']} of {order['qty']} {order['trading_symbol']}")
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__':
RefusedThenLoggedInAgainExample().run()
It prints:
Stand-in login number 1
Handshake headers: {'Authorization': 'indmoney-token-1', 'x-api-key': 'ind-client-7'}
Connecting to wss://ws-order-updates.indstocks.com/api/v1/ws/trades
WARNING Order updates handshake refused with HTTP 401, taken as a dead session.
INFO Order updates socket closed with status None: None
WARNING Logging in to INDmoney again.
Stand-in login number 2
WARNING Order updates socket disconnected. Reconnecting in 0 seconds.
Handshake headers: {'Authorization': 'indmoney-token-2', 'x-api-key': 'ind-client-7'}
Connecting to wss://ws-order-updates.indstocks.com/api/v1/ws/trades
Sent: {"action": "subscribe", "mode": "order_update"}
INFO Order updates socket opened and subscribed. Waiting for order updates.
Order IND2609250001 COMPLETE: SELL 5 of 5 TCS
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/indmoney.py
_connect
¶
Opens the websocket with the token in force now, presented in the handshake, and blocks until it closes.
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Source code in stock_brokers/websockets/indmoney.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
¶
Asks for order updates; the session is already authenticated by the handshake.
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/indmoney.py
_on_error
¶
Takes a refused handshake as a dead session, and reports any other error.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
websocket_connection
|
WebSocketApp
|
The connection that failed. |
required |
error
|
Exception
|
The error, which carries |
required |
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Source code in stock_brokers/websockets/indmoney.py
_on_message
¶
Hands a message's orders on, and logs acknowledgements and anything else.
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/indmoney.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. |