flattrade
¶
Flattrade's two websockets on the Noren platform: the market feed and the account's order updates.
Both connect to wss://piconnect.flattrade.in/PiConnectWSTp/, a separate connection each.
Flattrade permits only one websocket per session, so in production the order updates socket is not run and orders come from the poller alone.
The login.
FlattradeSession holds the FlattradeAPI both sockets read their credentials from, afresh on every connect, so a login made by any process is picked up.
Noren refuses a dead session inside an HTTP 200, so a constructor that returned is no proof of a working one: every login is confirmed by calling UserDetails, and a session it refuses raises.
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 user id.
The frame is a with the token named accesstoken: Noren also accepts a c frame naming it susertoken, and answers that NOT_OK for a token REST accepts, which reads like an expired session and is not one.
Only after the ak acknowledgement says OK (documented as "Ok" at one Noren broker and "OK" at another, 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 on the default interval is not enough and the server drops the connection.
Quotes.
FlattradeQuotesSocket 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, and a name that differs from the one held is handed to on_names before the frame's ticks.
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.
No tick is handed on until an instrument has a price.
Order updates.
FlattradeOrderUpdatesSocket subscribes to the whole account with {"t": "o", "actid": <user id>}, and every change to an order then arrives as an om message carrying the order book's fields.
Neither socket writes Redis.
The quotes socket hands each frame's new names to on_names and its ticks to on_ticks, and the order socket hands each frame's om updates to on_updates; the scripts in bin/flattrade/ do the writing.
Classes:
| Name | Description |
|---|---|
FlattradeSession |
The Flattrade login every socket authenticates with, confirmed against UserDetails and logged in again by one socket at a time. |
FlattradeQuotesSocket |
One Noren market feed websocket carrying the touchline and depth of a batch of instruments. |
FlattradeOrderUpdatesSocket |
Flattrade's Noren order update websocket, subscribed to the whole account once authenticated. |
FlattradeSession
¶
The Flattrade login every socket authenticates with, confirmed against UserDetails and logged in again by one socket at a time.
Constructs FlattradeAPI, which logs in when the stored session is dead, and confirms the session.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
logger
|
Logger
|
Where logins are reported. |
required |
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
When UserDetails refuses the session. |
Exception
|
Whatever |
Example programs
"""Builds a Flattrade session, reads its credentials and logs in again only when the failed token is still current.
Noren refuses a dead session inside an HTTP 200 answer, so a `FlattradeAPI` that constructed without error is no proof of a working session. `FlattradeSession` therefore confirms every login by calling UserDetails, and `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.
Constructing the real `FlattradeAPI` logs in to Flattrade, so the program registers a stand-in module under `stock_brokers.api.flattrade`. 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, and `post` answers the UserDetails call with `Ok`.
Notice that each login is followed by a UserDetails check, and that the second request with the same stale token makes no login.
Run it from the project root:
python examples/stock_brokers/websockets/flattrade/FlattradeSession/example_1_confirming_each_login.py
"""
import logging
import sys
import types
from stock_brokers.websockets.flattrade import (
FlattradeSession,
)
class LoginRecord:
"""The Flattrade 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.
refused_logins (set): The login numbers whose session UserDetails refuses.
"""
access_token = None
login_count = 0
refused_logins = set()
class StandInFlattradeAPI:
"""A stand-in for `FlattradeAPI` 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'flattrade-token-{LoginRecord.login_count}'
print(f'Stand-in login number {LoginRecord.login_count}')
self._settings = {
'username': 'FT012345',
}
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,
}
def post(self, url, timeout):
"""Answers the UserDetails call the session confirms every login with.
Args:
url (str): The endpoint the session calls.
timeout (int): The request timeout in seconds.
Returns:
dict: What the real class returns, with Noren's answer under `data`.
"""
status = 'Ok'
if LoginRecord.login_count in LoginRecord.refused_logins:
status = 'Not_Ok'
print(f'UserDetails called for {LoginRecord.access_token}: {status}')
return {
'data': {
'stat': status,
'uname': 'EXAMPLE USER',
},
}
class ConfirmingEachLoginExample:
"""Builds a Flattrade session and asks it to log in again twice with the same token.
Attributes:
session (FlattradeSession): 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.flattrade')
api_module.FlattradeAPI = StandInFlattradeAPI
sys.modules['stock_brokers.api.flattrade'] = api_module
self.session = FlattradeSession(logging.getLogger('flattrade.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__':
ConfirmingEachLoginExample().run()
It prints:
Stand-in login number 1
UserDetails called for flattrade-token-1: Ok
User FT012345 was refused with flattrade-token-1
WARNING Logging in to Flattrade again.
Stand-in login number 2
UserDetails called for flattrade-token-2: Ok
Credentials now: ('FT012345', 'flattrade-token-2')
Credentials after the second request: ('FT012345', 'flattrade-token-2')
Logins made: 2
"""Logs in to Flattrade again without checking and shows the error raised when UserDetails refuses the new session.
`FlattradeSession.log_in_again_without_checking` is what the order update socket calls after a refusal: it logs in whether or not the token was already replaced, then confirms the new session against UserDetails. If Noren refuses that session, the session raises `RuntimeError` with Noren's answer, which the socket's reconnect loop treats as a login that failed, so the socket gives up and its script exits for systemd to restart.
Constructing the real `FlattradeAPI` logs in to Flattrade, so the program registers a stand-in module under `stock_brokers.api.flattrade` whose second login is scripted to be refused by the pretend UserDetails call.
Notice that the first login is confirmed, that the second one is made even though nothing was refused, and that the `RuntimeError` carries Noren's `Not_Ok` answer.
Run it from the project root:
python examples/stock_brokers/websockets/flattrade/FlattradeSession/example_2_userdetails_refuses_the_new_session.py
"""
import logging
import sys
import types
from stock_brokers.websockets.flattrade import (
FlattradeSession,
)
class LoginRecord:
"""The Flattrade 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.
refused_logins (set): The login numbers whose session UserDetails refuses.
"""
access_token = None
login_count = 0
refused_logins = set()
class StandInFlattradeAPI:
"""A stand-in for `FlattradeAPI` 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'flattrade-token-{LoginRecord.login_count}'
print(f'Stand-in login number {LoginRecord.login_count}')
self._settings = {
'username': 'FT012345',
}
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,
}
def post(self, url, timeout):
"""Answers the UserDetails call the session confirms every login with.
Args:
url (str): The endpoint the session calls.
timeout (int): The request timeout in seconds.
Returns:
dict: What the real class returns, with Noren's answer under `data`.
"""
status = 'Ok'
if LoginRecord.login_count in LoginRecord.refused_logins:
status = 'Not_Ok'
print(f'UserDetails called for {LoginRecord.access_token}: {status}')
return {
'data': {
'stat': status,
'uname': 'EXAMPLE USER',
},
}
class UserdetailsRefusesTheNewSessionExample:
"""Logs in again unconditionally and catches the refusal of the new session.
Attributes:
session (FlattradeSession): The session being shown.
"""
def __init__(self):
"""Registers the stand-in API module, scripts the second login to be refused, 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.flattrade')
api_module.FlattradeAPI = StandInFlattradeAPI
sys.modules['stock_brokers.api.flattrade'] = api_module
LoginRecord.refused_logins = {
2,
}
self.session = FlattradeSession(logging.getLogger('flattrade.session'))
def run(self):
"""Logs in again without checking and prints the error.
Returns:
None: This method returns nothing.
"""
try:
self.session.log_in_again_without_checking()
except RuntimeError as error:
print(f'RuntimeError: {error}')
print(f'Logins made: {LoginRecord.login_count}')
if __name__ == '__main__':
UserdetailsRefusesTheNewSessionExample().run()
It prints:
Methods:
| Name | Description |
|---|---|
credentials |
The user id and the access token in force now, which may be one another process has just obtained. |
log_in_again |
Logs in again and confirms the session, unless another socket or process already replaced the token that failed. |
log_in_again_without_checking |
Logs in again and confirms the session, whether or not the token was already replaced. |
Source code in stock_brokers/websockets/flattrade.py
credentials
¶
The user id and the access token in force now, which may be one another process has just obtained.
Returns:
| Type | Description |
|---|---|
tuple
|
The user id and the access 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/flattrade.py
log_in_again
¶
Logs in again and confirms the session, 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 |
|---|---|
RuntimeError
|
When UserDetails refuses the new session. |
Exception
|
Whatever |
Example usage
Source code in stock_brokers/websockets/flattrade.py
log_in_again_without_checking
¶
Logs in again and confirms the session, whether or not the token was already replaced.
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
When UserDetails refuses the new session. |
Exception
|
Whatever |
Example usage
Source code in stock_brokers/websockets/flattrade.py
_log_in
¶
Logs in by constructing FlattradeAPI again and confirms the session; the caller holds the lock.
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
When UserDetails refuses the new session. |
Exception
|
Whatever |
Source code in stock_brokers/websockets/flattrade.py
_confirm
¶
Checks that UserDetails serves the session, since Noren refuses a dead session inside an HTTP 200.
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
When UserDetails refuses the session. |
Source code in stock_brokers/websockets/flattrade.py
FlattradeQuotesSocket
¶
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 updated as Noren names instruments. |
required |
session
|
FlattradeSession
|
The shared login. |
required |
on_names
|
callable
|
Called with a dictionary of tokens to their new names when a frame names instruments, on this socket's thread. |
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
"""Authenticates a Flattrade quotes socket and merges Noren's incremental feed into full ticks.
A `FlattradeQuotesSocket` 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.flattrade` module lets the real `FlattradeSession` log in and confirm its session against a pretend UserDetails call.
Notice that the new name is handed to `on_names` before the first 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/flattrade/FlattradeQuotesSocket/example_1_merging_incremental_updates.py
"""
import json
import logging
import sys
import types
from stock_brokers.websockets.flattrade import (
FlattradeQuotesSocket,
FlattradeSession,
)
class LoginRecord:
"""The Flattrade 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.
refused_logins (set): The login numbers whose session UserDetails refuses.
"""
access_token = None
login_count = 0
refused_logins = set()
class StandInFlattradeAPI:
"""A stand-in for `FlattradeAPI` 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'flattrade-token-{LoginRecord.login_count}'
print(f'Stand-in login number {LoginRecord.login_count}')
self._settings = {
'username': 'FT012345',
}
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,
}
def post(self, url, timeout):
"""Answers the UserDetails call the session confirms every login with.
Args:
url (str): The endpoint the session calls.
timeout (int): The request timeout in seconds.
Returns:
dict: What the real class returns, with Noren's answer under `data`.
"""
status = 'Ok'
if LoginRecord.login_count in LoginRecord.refused_logins:
status = 'Not_Ok'
print(f'UserDetails called for {LoginRecord.access_token}: {status}')
return {
'data': {
'stat': status,
'uname': 'EXAMPLE USER',
},
}
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 Flattrade quotes socket against an acknowledgement and one update.
Attributes:
socket (FlattradeQuotesSocket): 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.flattrade')
api_module.FlattradeAPI = StandInFlattradeAPI
sys.modules['stock_brokers.api.flattrade'] = api_module
connect_acknowledgement = {
't': 'ak',
's': 'OK',
'uid': 'FT012345',
}
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('flattrade.quotes')
tokens = [
'NSE|2885',
]
names = {}
session = FlattradeSession(logger)
self.socket = FlattradeQuotesSocket('socket_0', tokens, names, session, self.print_names, self.print_ticks, logger)
websocket_module.when_finished = self.socket.close
def print_names(self, named):
"""Prints the instruments a frame newly named.
Args:
named (dict): Tokens to their new names.
Returns:
None: This method returns nothing.
"""
print(f'Names learned: {named}')
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:
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
UserDetails called for flattrade-token-1: Ok
Connecting to wss://piconnect.flattrade.in/PiConnectWSTp/
Sent: {"t": "a", "uid": "FT012345", "actid": "FT012345", "source": "API", "accesstoken": "flattrade-token-1"}
INFO socket_0 opened. Authenticating.
Sent: {"t": "d", "k": "NSE|2885"}
INFO socket_0 authenticated and subscribed to 1 instrument(s).
Names learned: {'NSE|2885': '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 Flattrade 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`. `FlattradeQuotesSocket` then closes the connection and its reconnect loop asks the shared `FlattradeSession` to log in again, which the session confirms against UserDetails 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.flattrade` module counts each `FlattradeAPI` construction as a login. The backoff is set to zero seconds.
Notice the second login and its UserDetails check, 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/flattrade/FlattradeQuotesSocket/example_2_refused_acknowledgement.py
"""
import json
import logging
import sys
import types
from stock_brokers.websockets.flattrade import (
FlattradeQuotesSocket,
FlattradeSession,
)
class LoginRecord:
"""The Flattrade 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.
refused_logins (set): The login numbers whose session UserDetails refuses.
"""
access_token = None
login_count = 0
refused_logins = set()
class StandInFlattradeAPI:
"""A stand-in for `FlattradeAPI` 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'flattrade-token-{LoginRecord.login_count}'
print(f'Stand-in login number {LoginRecord.login_count}')
self._settings = {
'username': 'FT012345',
}
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,
}
def post(self, url, timeout):
"""Answers the UserDetails call the session confirms every login with.
Args:
url (str): The endpoint the session calls.
timeout (int): The request timeout in seconds.
Returns:
dict: What the real class returns, with Noren's answer under `data`.
"""
status = 'Ok'
if LoginRecord.login_count in LoginRecord.refused_logins:
status = 'Not_Ok'
print(f'UserDetails called for {LoginRecord.access_token}: {status}')
return {
'data': {
'stat': status,
'uname': 'EXAMPLE USER',
},
}
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 Flattrade quotes socket through a refused connect frame and a good connection.
Attributes:
socket (FlattradeQuotesSocket): 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.flattrade')
api_module.FlattradeAPI = StandInFlattradeAPI
sys.modules['stock_brokers.api.flattrade'] = 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('flattrade.quotes')
tokens = [
'MCX|565899',
'MCX|565900',
]
names = {}
session = FlattradeSession(logger)
self.socket = FlattradeQuotesSocket('socket_0', tokens, names, session, self.print_names, self.print_ticks, logger)
self.socket.MIN_BACKOFF_SECONDS = 0
websocket_module.when_finished = self.socket.close
def print_names(self, named):
"""Prints the instruments a frame newly named.
Args:
named (dict): Tokens to their new names.
Returns:
None: This method returns nothing.
"""
print(f'Names learned: {named}')
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:
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
UserDetails called for flattrade-token-1: Ok
Connecting to wss://piconnect.flattrade.in/PiConnectWSTp/
Sent: {"t": "a", "uid": "FT012345", "actid": "FT012345", "source": "API", "accesstoken": "flattrade-token-1"}
INFO socket_0 opened. Authenticating.
ERROR socket_0 authentication rejected: {'t': 'ak', 's': 'NOT_OK', 'emsg': 'Session Expired : Invalid Session Key'}
INFO socket_0 closed with status 1000: normal closure
WARNING Logging in to Flattrade again.
Stand-in login number 2
UserDetails called for flattrade-token-2: Ok
WARNING socket_0 disconnected. Reconnecting in 0 seconds.
Connecting to wss://piconnect.flattrade.in/PiConnectWSTp/
Sent: {"t": "a", "uid": "FT012345", "actid": "FT012345", "source": "API", "accesstoken": "flattrade-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).
Names learned: {'MCX|565899': '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
Names learned: {'MCX|565900': '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/flattrade.py
_connect
¶
Opens the websocket with the credentials in force now and blocks until it closes, sending Noren's heartbeat meanwhile.
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Source code in stock_brokers/websockets/flattrade.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/flattrade.py
_on_open
¶
Sends the connect frame; the subscription waits for its 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/flattrade.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/flattrade.py
_on_message
¶
Handles one Noren frame, handing its new names on first and then its ticks.
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/flattrade.py
_parse
¶
Reads one Noren frame, subscribing on the connect acknowledgement and learning names on the way.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
bytes | str
|
The frame. |
required |
Returns:
| Type | Description |
|---|---|
tuple
|
The frame's ticks as a list, and the instruments it newly names as a dictionary of token to name. |
Source code in stock_brokers/websockets/flattrade.py
_build_tick
¶
Builds a normalized tick from the merged state of an instrument.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
instrument_token
|
str
|
The |
required |
state
|
dict
|
The instrument's merged fields. |
required |
name
|
str | None
|
The instrument's name, or None to key the tick by its token. |
required |
Returns:
| Type | Description |
|---|---|
dict
|
The tick. |
Source code in stock_brokers/websockets/flattrade.py
_number
¶
A Noren string field as a number, treating blanks and placeholders as missing.
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/flattrade.py
_trade_time
¶
Noren's last trade time as an epoch, whichever way it is spelled.
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/flattrade.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
FlattradeOrderUpdatesSocket
¶
Bases: BrokerWebsocket
Flattrade's Noren order update websocket, subscribed to the whole account once authenticated.
Sets up the order updates socket.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
FlattradeSession
|
The login. |
required |
on_updates
|
callable
|
Called on this socket's thread with each frame's |
required |
logger
|
Logger
|
Where the connection reports. |
required |
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Example programs
"""Authenticates Flattrade's order update socket, subscribes to the account and prints the order updates it hands on.
`FlattradeOrderUpdatesSocket` 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. In production Flattrade permits one websocket per session, so this socket is not run there; the program shows what it does when it is.
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.flattrade` module lets the real `FlattradeSession` log in and confirm its session without reaching Flattrade.
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/flattrade/FlattradeOrderUpdatesSocket/example_1_subscribing_to_the_account.py
"""
import json
import logging
import sys
import types
from stock_brokers.websockets.flattrade import (
FlattradeOrderUpdatesSocket,
FlattradeSession,
)
class LoginRecord:
"""The Flattrade 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.
refused_logins (set): The login numbers whose session UserDetails refuses.
"""
access_token = None
login_count = 0
refused_logins = set()
class StandInFlattradeAPI:
"""A stand-in for `FlattradeAPI` 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'flattrade-token-{LoginRecord.login_count}'
print(f'Stand-in login number {LoginRecord.login_count}')
self._settings = {
'username': 'FT012345',
}
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,
}
def post(self, url, timeout):
"""Answers the UserDetails call the session confirms every login with.
Args:
url (str): The endpoint the session calls.
timeout (int): The request timeout in seconds.
Returns:
dict: What the real class returns, with Noren's answer under `data`.
"""
status = 'Ok'
if LoginRecord.login_count in LoginRecord.refused_logins:
status = 'Not_Ok'
print(f'UserDetails called for {LoginRecord.access_token}: {status}')
return {
'data': {
'stat': status,
'uname': 'EXAMPLE USER',
},
}
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 Flattrade order update socket against one scripted connection.
Attributes:
socket (FlattradeOrderUpdatesSocket): 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.flattrade')
api_module.FlattradeAPI = StandInFlattradeAPI
sys.modules['stock_brokers.api.flattrade'] = 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('flattrade.orders')
self.socket = FlattradeOrderUpdatesSocket(FlattradeSession(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': 'FT012345',
'actid': 'FT012345',
'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
UserDetails called for flattrade-token-1: Ok
Connecting to wss://piconnect.flattrade.in/PiConnectWSTp/
Sent: {"t": "a", "uid": "FT012345", "actid": "FT012345", "source": "API", "accesstoken": "flattrade-token-1"}
INFO Order updates socket opened. Authenticating.
Sent: {"t": "o", "actid": "FT012345"}
INFO Authenticated and subscribed to order updates. 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 Flattrade's order update socket logging in again, without checking, after Noren rejects its connect frame.
When the `ak` acknowledgement is not `OK`, `FlattradeOrderUpdatesSocket` closes the connection, and its reconnect loop calls `FlattradeSession.log_in_again_without_checking`, which logs in and confirms the new session against UserDetails 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.flattrade` module counts each `FlattradeAPI` construction as a login. The backoff is set to zero seconds.
Notice the error line for the rejection, the second login with its UserDetails check, and the new token in the second connect frame.
Run it from the project root:
python examples/stock_brokers/websockets/flattrade/FlattradeOrderUpdatesSocket/example_2_rejected_then_logged_in_again.py
"""
import json
import logging
import sys
import types
from stock_brokers.websockets.flattrade import (
FlattradeOrderUpdatesSocket,
FlattradeSession,
)
class LoginRecord:
"""The Flattrade 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.
refused_logins (set): The login numbers whose session UserDetails refuses.
"""
access_token = None
login_count = 0
refused_logins = set()
class StandInFlattradeAPI:
"""A stand-in for `FlattradeAPI` 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'flattrade-token-{LoginRecord.login_count}'
print(f'Stand-in login number {LoginRecord.login_count}')
self._settings = {
'username': 'FT012345',
}
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,
}
def post(self, url, timeout):
"""Answers the UserDetails call the session confirms every login with.
Args:
url (str): The endpoint the session calls.
timeout (int): The request timeout in seconds.
Returns:
dict: What the real class returns, with Noren's answer under `data`.
"""
status = 'Ok'
if LoginRecord.login_count in LoginRecord.refused_logins:
status = 'Not_Ok'
print(f'UserDetails called for {LoginRecord.access_token}: {status}')
return {
'data': {
'stat': status,
'uname': 'EXAMPLE USER',
},
}
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 Flattrade order update socket through a rejection and a good connection.
Attributes:
socket (FlattradeOrderUpdatesSocket): 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.flattrade')
api_module.FlattradeAPI = StandInFlattradeAPI
sys.modules['stock_brokers.api.flattrade'] = 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('flattrade.orders')
self.socket = FlattradeOrderUpdatesSocket(FlattradeSession(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': 'FT012345',
'actid': 'FT012345',
'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
UserDetails called for flattrade-token-1: Ok
Connecting to wss://piconnect.flattrade.in/PiConnectWSTp/
Sent: {"t": "a", "uid": "FT012345", "actid": "FT012345", "source": "API", "accesstoken": "flattrade-token-1"}
INFO Order updates socket opened. Authenticating.
ERROR Authentication rejected: {'t': 'ak', 's': 'NOT_OK'}
INFO Order updates socket closed with status 1000: normal closure
WARNING Logging in to Flattrade again.
Stand-in login number 2
UserDetails called for flattrade-token-2: Ok
WARNING Order updates socket disconnected. Reconnecting in 0 seconds.
Connecting to wss://piconnect.flattrade.in/PiConnectWSTp/
Sent: {"t": "a", "uid": "FT012345", "actid": "FT012345", "source": "API", "accesstoken": "flattrade-token-2"}
INFO Order updates socket opened. Authenticating.
Sent: {"t": "o", "actid": "FT012345"}
INFO Authenticated and subscribed to order updates. 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/flattrade.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/flattrade.py
_log_in_again
¶
Logs in again and confirms the session, without checking whether another process already replaced the token.
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Source code in stock_brokers/websockets/flattrade.py
_on_open
¶
Sends the connect frame with the token in force now; the subscription waits for its 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/flattrade.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/flattrade.py
_on_message
¶
Subscribes once authenticated, and hands each frame's order updates on.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
websocket_connection
|
WebSocketApp
|
The connection the frame 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/flattrade.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. |