groww
¶
Groww's two websockets, both NATS over a websocket at wss://socket-api.groww.in: the market feed and the account's order and position updates.
The login and the socket token.
GrowwSession holds the GrowwAPI both sockets read the REST access token from, afresh on every connect, so a login made by any process is picked up.
Before connecting, a socket generates a fresh ed25519 key pair and exchanges its public key, in NATS NKEY form, for a short-lived socket JWT and a subscription id at POST https://api.groww.in/v1/api/apex/v1/socket/token/create/, authorized with the access token.
HTTP 401 or 403 there means the access token is dead, which the socket treats as a refused login.
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.
NATS.
The server opens with an INFO frame carrying a nonce; the socket answers CONNECT with the JWT and the nonce signed by its private key, then PING, then one SUB per subject.
NATS is line based: control lines end with CRLF, and a MSG line is followed by a payload of the declared length, and a websocket frame may split or join those, so GrowwNatsBuffer buffers the stream and hands back only whole messages.
A server PING is answered with PONG, and an -ERR naming an authorization failure is a refused login.
Quotes.
GrowwQuotesSocket subscribes a price subject and a depth subject per instrument, such as /ld/eq/nse/price_detailed.2885 and /ld/eq/nse/book.2885, with /ld/fo/... for derivatives.
Payloads are protobuf StocksSocketResponseProtoDto messages, whose classes are built at runtime from Groww's file descriptor, carried here serialized, in a descriptor pool of their own.
Price and depth arrive separately and are merged per instrument.
Prices are in paise and are divided into rupees, and quantities arrive as doubles and are rounded to whole numbers.
An instrument without a name takes the symbol Groww puts in the payload.
The quote socket gives up only after six more failed connects, not on the first refusal after logging in again.
Order updates.
GrowwOrderUpdatesSocket subscribes the account's subjects, keyed by the subscription id: equity and derivatives orders (OrderDetailsBroadCastDto) and derivatives positions (PositionDetailProto).
Each message is decoded to a plain dictionary with Groww's own field names, enum fields by name, and every field present even when zero.
Neither socket writes Redis.
The quotes socket hands each frame's ticks to on_ticks, and the order socket hands each frame's decoded orders and positions to on_updates; the scripts in bin/groww/ do the writing.
Classes:
| Name | Description |
|---|---|
GrowwSocketRefused |
Groww refused the socket token request, which means the access token is dead. |
GrowwMessageClasses |
The protobuf message classes Groww's payloads decode into, built from the embedded file descriptors. |
GrowwNkeyPair |
An ed25519 key pair in NATS NKEY form: the public key as a user NKEY, and nonce signing. |
GrowwNatsBuffer |
The NATS byte stream of one connection, buffered so that only whole lines and messages come out. |
GrowwSession |
The Groww login every socket authenticates with, logged in again by one socket at a time. |
GrowwQuotesSocket |
One Groww NATS websocket carrying the price and depth of a batch of instruments. |
GrowwOrderUpdatesSocket |
Groww's order and position update stream for the account, over NATS. |
GrowwSocketRefused
¶
Bases: Exception
Groww refused the socket token request, which means the access token is dead.
Example programs
"""Raises and catches `GrowwSocketRefused`, the error that marks a dead Groww access token.
`GrowwSocketRefused` is raised when Groww refuses a socket token request with HTTP 401 or 403, which means the access token is dead. Both Groww sockets catch it in their connect step and turn it into a refused login, so their reconnect loops log in again. It is a plain `Exception` subclass whose message says the HTTP status and the start of Groww's answer.
This program raises it with the message shape the real code uses and catches it, both as itself and as a plain `Exception`. It needs no stand-ins.
Notice that the class name is what tells a refused login apart from any other failure.
Run it from the project root:
python examples/stock_brokers/websockets/groww/GrowwSocketRefused/example_1_raising_and_catching.py
"""
from stock_brokers.websockets.groww import (
GrowwSocketRefused,
)
class RaisingAndCatchingExample:
"""Raises and catches a Groww socket refusal.
Attributes:
message (str): The message the refusal carries.
"""
def __init__(self):
"""Builds the message the way the session does.
Returns:
None: This method returns nothing.
"""
self.message = 'HTTP 403: {"message": "Forbidden"}'
def refuse(self):
"""Raises the refusal.
Returns:
None: This method returns nothing.
Raises:
GrowwSocketRefused: Always.
"""
raise GrowwSocketRefused(self.message)
def run(self):
"""Catches the refusal twice, once by its own class and once as any exception.
Returns:
None: This method returns nothing.
"""
try:
self.refuse()
except GrowwSocketRefused as refusal:
print(f'Caught as a refused login: {refusal}')
try:
self.refuse()
except Exception as error:
print(f'Caught as {type(error).__name__}, a subclass of Exception: {isinstance(error, Exception)}')
if __name__ == '__main__':
RaisingAndCatchingExample().run()
It prints:
"""Catches the `GrowwSocketRefused` a Groww session raises when the socket token endpoint answers HTTP 401.
In practice `GrowwSocketRefused` comes from `GrowwSession.socket_token_payload`, which raises it for HTTP 401 or 403 and lets any other error status raise as `requests` does. A socket catches it and logs in again instead of counting an ordinary connection failure.
Constructing the real `GrowwAPI` logs in to Groww, so the program registers a stand-in module under `stock_brokers.api.groww`, and a stand-in `requests` package answers the socket token request with HTTP 401.
Notice that the message carries the status and Groww's answer.
Run it from the project root:
python examples/stock_brokers/websockets/groww/GrowwSocketRefused/example_2_from_the_token_endpoint.py
"""
import json
import logging
import sys
import types
from stock_brokers.websockets.groww import (
GrowwNkeyPair,
GrowwSession,
GrowwSocketRefused,
)
class LoginRecord:
"""The Groww 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 StandInGrowwAPI:
"""A stand-in for `GrowwAPI` 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'groww-token-{LoginRecord.login_count}'
print(f'Stand-in login number {LoginRecord.login_count}')
self._settings = {
'api_key': 'groww-api-key',
}
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 SocketTokenResponse:
"""A stand-in for the `requests` response of Groww's socket token endpoint.
Attributes:
status_code (int): The HTTP status.
body (dict): The JSON body.
text (str): The body as text.
"""
def __init__(self, status_code, body):
"""Keeps the answer.
Args:
status_code (int): The HTTP status.
body (dict): The JSON body.
Returns:
None: This method returns nothing.
"""
self.status_code = status_code
self.body = body
self.text = json.dumps(body)
def json(self):
"""The JSON body.
Returns:
dict: The body.
"""
return self.body
def raise_for_status(self):
"""Does nothing, because every scripted answer that reaches it succeeded.
Returns:
None: This method returns nothing.
"""
class StandInRequests:
"""A stand-in for the `requests` package that answers socket token requests in turn.
Attributes:
statuses (list): The HTTP status of each answer, in order.
"""
def __init__(self, statuses):
"""Keeps the scripted statuses.
Args:
statuses (list): The HTTP status of each answer, in order.
Returns:
None: This method returns nothing.
"""
self.statuses = statuses
def post(self, url, headers, json, timeout):
"""Answers one socket token request.
Args:
url (str): The endpoint.
headers (dict): The request headers, carrying the access token.
json (dict): The request body with the public key.
timeout (int): The request timeout in seconds.
Returns:
SocketTokenResponse: The answer.
"""
status = self.statuses.pop(0)
print(f"Socket token request with {headers['Authorization']} for a key starting {json['socketKey'][0]}: HTTP {status}")
if status != 200:
body = {
'message': 'Unauthorized',
}
return SocketTokenResponse(status, body)
body = {
'payload': {
'token': 'socket-jwt',
'subscriptionId': 'subscription-77',
},
}
return SocketTokenResponse(status, body)
class FromTheTokenEndpointExample:
"""Requests a socket token with a dead access token.
Attributes:
session (GrowwSession): The session that makes the request.
"""
def __init__(self):
"""Registers the stand-ins 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.groww')
api_module.GrowwAPI = StandInGrowwAPI
sys.modules['stock_brokers.api.groww'] = api_module
sys.modules['requests'] = StandInRequests([
401,
])
self.session = GrowwSession(logging.getLogger('groww.session'))
def run(self):
"""Requests a socket token and prints the refusal.
Returns:
None: This method returns nothing.
"""
try:
self.session.socket_token_payload(GrowwNkeyPair())
except GrowwSocketRefused as refusal:
print(f'GrowwSocketRefused: {refusal}')
if __name__ == '__main__':
FromTheTokenEndpointExample().run()
It prints:
GrowwMessageClasses
¶
The protobuf message classes Groww's payloads decode into, built from the embedded file descriptors.
Example programs
"""Builds Groww's market feed protobuf class, round-trips a price message through bytes, and turns it into a dictionary.
Groww's market feed payloads are protobuf `StocksSocketResponseProtoDto` messages. `GrowwMessageClasses.stocks_response_class` builds that class at runtime from Groww's file descriptor, which the module carries serialized, in a descriptor pool of its own so it cannot clash with any other protobuf classes. `as_dict` turns any decoded message into a plain dictionary with Groww's own field names, enums by name and every field present.
This program fills in a price message for RELIANCE, serializes it as the server would, decodes the bytes with the class, and prints the dictionary. It needs no stand-ins.
Notice that fields the program never set, such as `openInterest`, still appear with zero, and that `segment` and `exchange` come out as the names `CASH` and `NSE`.
Run it from the project root:
python examples/stock_brokers/websockets/groww/GrowwMessageClasses/example_1_market_feed_message.py
"""
from stock_brokers.websockets.groww import (
GrowwMessageClasses,
)
class MarketFeedMessageExample:
"""Round-trips one Groww market feed message.
Attributes:
message_classes (GrowwMessageClasses): The class builder being shown.
"""
def __init__(self):
"""Builds the class builder.
Returns:
None: This method returns nothing.
"""
self.message_classes = GrowwMessageClasses()
def run(self):
"""Builds, serializes, decodes and prints a price message.
Returns:
None: This method returns nothing.
"""
response_class = self.message_classes.stocks_response_class()
print(f'Class: {response_class.DESCRIPTOR.full_name}')
message = response_class()
message.symbol = 'RELIANCE'
message.segment = 0
message.exchange = 1
message.stockLivePrice.ltp = 295050
message.stockLivePrice.close = 291000
message.stockLivePrice.volume = 1204500
payload = message.SerializeToString()
print(f'Serialized to {len(payload)} bytes')
decoded = response_class.FromString(payload)
dictionary = self.message_classes.as_dict(decoded)
print(f"symbol={dictionary['symbol']} segment={dictionary['segment']} exchange={dictionary['exchange']}")
for field, value in sorted(dictionary['stockLivePrice'].items()):
print(f' {field}: {value}')
if __name__ == '__main__':
MarketFeedMessageExample().run()
It prints:
Class: stocksData.response.StocksSocketResponseProtoDto
Serialized to 41 bytes
symbol=RELIANCE segment=CASH exchange=NSE
avgPrice: 0.0
bidQty: 0.0
close: 291000.0
high: 0.0
highPriceRange: 0.0
highTradeRange: 0.0
low: 0.0
lowPriceRange: 0.0
lowTradeRange: 0.0
ltp: 295050.0
offerQty: 0.0
open: 0.0
openInterest: 0.0
tsInMillis: 0.0
value: 0.0
volume: 1204500.0
"""Builds Groww's order and position protobuf classes and decodes one of each into a dictionary.
The order update stream carries two kinds of protobuf payload: `OrderDetailsBroadCastDto` for equity and derivatives orders and `PositionDetailProto` for derivatives positions. `GrowwMessageClasses.order_and_position_classes` builds both from their file descriptors in one pool and returns them as a pair, and `as_dict` turns a decoded message into the dictionary the order socket hands on.
This program builds an executed INFY buy and a NIFTY call position, serializes and decodes each, and prints selected fields of the dictionaries. It needs no stand-ins.
Notice that the order status and side come out by name (`EXECUTED`, `B`), that the 64 bit price comes out as text, as protobuf JSON always writes 64 bit integers, and that the position keeps its nested `symbolData` and `positionInfo`.
Run it from the project root:
python examples/stock_brokers/websockets/groww/GrowwMessageClasses/example_2_order_and_position_messages.py
"""
from stock_brokers.websockets.groww import (
GrowwMessageClasses,
)
class OrderAndPositionMessagesExample:
"""Round-trips one Groww order message and one position message.
Attributes:
message_classes (GrowwMessageClasses): The class builder being shown.
"""
def __init__(self):
"""Builds the class builder.
Returns:
None: This method returns nothing.
"""
self.message_classes = GrowwMessageClasses()
def run(self):
"""Builds, serializes, decodes and prints an order and a position.
Returns:
None: This method returns nothing.
"""
order_class, position_class = self.message_classes.order_and_position_classes()
print(f'Classes: {order_class.DESCRIPTOR.full_name} and {position_class.DESCRIPTOR.full_name}')
broadcast = order_class()
broadcast.orderDetailUpdateDto.growwOrderId = 'GMK2609250001'
broadcast.orderDetailUpdateDto.qty = 10
broadcast.orderDetailUpdateDto.filledQty = 10
broadcast.orderDetailUpdateDto.price = 150000
broadcast.orderDetailUpdateDto.orderStatus = 6
order = self.message_classes.as_dict(order_class.FromString(broadcast.SerializeToString()))
detail = order['orderDetailUpdateDto']
print(f"Order {detail['growwOrderId']}: status={detail['orderStatus']} side={detail['buySell']} filled={detail['filledQty']} price={detail['price']!r}")
position = position_class()
position.symbolData.contractId = 'NIFTY26SEP25000CE'
position.symbolData.equityType = 2
position.positionInfo.NSE.creditQty = 75
decoded = self.message_classes.as_dict(position_class.FromString(position.SerializeToString()))
print(f"Position {decoded['symbolData']['contractId']}: type={decoded['symbolData']['equityType']} NSE={decoded['positionInfo']['NSE']}")
if __name__ == '__main__':
OrderAndPositionMessagesExample().run()
It prints:
Methods:
| Name | Description |
|---|---|
stocks_response_class |
The |
order_and_position_classes |
The |
as_dict |
A protobuf message as a JSON-ready dictionary: Groww's field names, enums by name, every field present. |
stocks_response_class
¶
The stocksData.response.StocksSocketResponseProtoDto class the market feed's payloads decode into.
Returns:
| Type | Description |
|---|---|
type
|
The message class. |
Example usage
Source code in stock_brokers/websockets/groww.py
order_and_position_classes
¶
The OrderDetailsBroadCastDto and PositionDetailProto classes the order stream's payloads decode into.
Returns:
| Type | Description |
|---|---|
tuple
|
The order message class and the position message class. |
Example usage
order_class, position_class = self.message_classes.order_and_position_classes()
Source code in stock_brokers/websockets/groww.py
as_dict
¶
A protobuf message as a JSON-ready dictionary: Groww's field names, enums by name, every field present.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
Message
|
The decoded message. |
required |
Returns:
| Type | Description |
|---|---|
dict
|
The message. |
Example usage
order = self.message_classes.as_dict(order_class.FromString(broadcast.SerializeToString()))
decoded = self.message_classes.as_dict(position_class.FromString(position.SerializeToString()))
Source code in stock_brokers/websockets/groww.py
GrowwNkeyPair
¶
An ed25519 key pair in NATS NKEY form: the public key as a user NKEY, and nonce signing.
Attributes:
| Name | Type | Description |
|---|---|---|
public_key |
str
|
The public key as a NATS user NKEY. |
Generates a fresh key pair.
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Example programs
"""Generates a Groww NATS key pair, signs a server nonce with it, and verifies the signature.
Before each Groww connection a socket generates a fresh ed25519 key pair, sends its public key as a NATS user NKEY to obtain a socket JWT, and later signs the nonce from the server's INFO line with the private key. `GrowwNkeyPair.public_key` is the NKEY: base32 of a user prefix byte, the raw public key and a CRC-16 checksum, so it always starts with `U` and has 56 characters. `signed_nonce` returns the signature base64url encoded without padding, as NATS expects.
The key is random on every run, so this program prints properties of the key and the signature rather than their values, and checks the signature by decoding the NKEY back to the raw public key and verifying with the `cryptography` package, the same library the class uses. It needs no stand-ins.
Notice that the signature verifies against the nonce it signed and not against a different one.
Run it from the project root:
python examples/stock_brokers/websockets/groww/GrowwNkeyPair/example_1_signing_a_nonce.py
"""
import base64
from cryptography.exceptions import (
InvalidSignature,
)
from cryptography.hazmat.primitives.asymmetric.ed25519 import (
Ed25519PublicKey,
)
from stock_brokers.websockets.groww import (
GrowwNkeyPair,
)
class SigningANonceExample:
"""Signs a nonce with a fresh key pair and verifies it.
Attributes:
key_pair (GrowwNkeyPair): The key pair being shown.
"""
def __init__(self):
"""Generates the key pair.
Returns:
None: This method returns nothing.
"""
self.key_pair = GrowwNkeyPair()
def verifies(self, signature, nonce):
"""Says whether a signature verifies against a nonce with the key pair's public key.
Args:
signature (str): The base64url signature without padding.
nonce (str): The nonce.
Returns:
bool: True when the signature is valid for the nonce.
"""
public_key_bytes = base64.b32decode(self.key_pair.public_key)[1:33]
public_key = Ed25519PublicKey.from_public_bytes(public_key_bytes)
raw_signature = base64.urlsafe_b64decode(signature + '==')
try:
public_key.verify(raw_signature, nonce.encode())
except InvalidSignature:
return False
return True
def run(self):
"""Prints the key's form, signs a nonce, and verifies the signature against two nonces.
Returns:
None: This method returns nothing.
"""
public_key = self.key_pair.public_key
print(f'Public key starts with {public_key[0]} and has {len(public_key)} characters')
signature = self.key_pair.signed_nonce('example-nonce')
print(f'Signature has {len(signature)} characters and no padding: {not signature.endswith("=")}')
print(f"Verifies against the signed nonce: {self.verifies(signature, 'example-nonce')}")
print(f"Verifies against another nonce: {self.verifies(signature, 'other-nonce')}")
if __name__ == '__main__':
SigningANonceExample().run()
It prints:
"""Shows that every Groww key pair is fresh, and that a nonce signs the same whether given as text or bytes.
A Groww socket makes a new `GrowwNkeyPair` for every connection, so a socket JWT obtained for one connection cannot be reused with another key. `signed_nonce` accepts the nonce as text or bytes, encoding text first, and ed25519 signatures are deterministic, so the same key signs the same nonce identically either way.
The keys are random, so this program compares them rather than printing them. It needs no stand-ins.
Notice that the two public keys differ, and that the text and bytes nonces give the same signature.
Run it from the project root:
python examples/stock_brokers/websockets/groww/GrowwNkeyPair/example_2_fresh_keys_each_connection.py
"""
from stock_brokers.websockets.groww import (
GrowwNkeyPair,
)
class FreshKeysEachConnectionExample:
"""Compares two key pairs and two ways of passing a nonce.
Attributes:
first_key_pair (GrowwNkeyPair): The key pair of one connection.
second_key_pair (GrowwNkeyPair): The key pair of the next connection.
"""
def __init__(self):
"""Generates two key pairs.
Returns:
None: This method returns nothing.
"""
self.first_key_pair = GrowwNkeyPair()
self.second_key_pair = GrowwNkeyPair()
def run(self):
"""Prints whether the keys differ and whether the two nonce forms sign alike.
Returns:
None: This method returns nothing.
"""
print(f'The two public keys differ: {self.first_key_pair.public_key != self.second_key_pair.public_key}')
from_text = self.first_key_pair.signed_nonce('example-nonce')
from_bytes = self.first_key_pair.signed_nonce(b'example-nonce')
print(f'Text and bytes nonces sign alike: {from_text == from_bytes}')
other_key = self.second_key_pair.signed_nonce('example-nonce')
print(f'Another key signs differently: {from_text != other_key}')
if __name__ == '__main__':
FreshKeysEachConnectionExample().run()
It prints:
Methods:
| Name | Description |
|---|---|
signed_nonce |
A server nonce signed with the private key, base64url encoded without padding, as NATS expects. |
Source code in stock_brokers/websockets/groww.py
signed_nonce
¶
A server nonce signed with the private key, base64url encoded without padding, as NATS expects.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
nonce
|
str | bytes
|
The nonce from the INFO frame. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The signature. |
Example usage
from_text = self.first_key_pair.signed_nonce('example-nonce')
from_bytes = self.first_key_pair.signed_nonce(b'example-nonce')
Source code in stock_brokers/websockets/groww.py
_crc16
¶
CRC-16/XMODEM, as NATS computes it over NKEY material.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
data
|
bytes
|
The bytes to checksum. |
required |
Returns:
| Type | Description |
|---|---|
int
|
The checksum. |
Source code in stock_brokers/websockets/groww.py
GrowwNatsBuffer
¶
The NATS byte stream of one connection, buffered so that only whole lines and messages come out.
Starts with nothing buffered.
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Example programs
"""Feeds one websocket frame of NATS traffic to the buffer and prints the events it takes out.
NATS is line based: control lines end with CRLF, and a `MSG` line is followed by a payload of the length it declares. `GrowwNatsBuffer.feed` adds a websocket frame to the stream and returns, in order, an `info` event with the decoded INFO line, a `ping` event, an `error` event for an `-ERR` line, and a `message` event with the subject and payload of each whole `MSG`. Other control lines, such as `+OK` or `PONG`, produce nothing.
This program feeds one frame holding an INFO line, a PONG, a PING and a message. It needs no stand-ins.
Notice that the payload comes out as bytes exactly as long as the `MSG` line declared, even though it contains a CRLF of its own.
Run it from the project root:
python examples/stock_brokers/websockets/groww/GrowwNatsBuffer/example_1_control_lines_and_a_message.py
"""
from stock_brokers.websockets.groww import (
GrowwNatsBuffer,
)
class ControlLinesAndAMessageExample:
"""Feeds one frame to a NATS buffer.
Attributes:
buffer (GrowwNatsBuffer): The buffer being shown.
"""
def __init__(self):
"""Builds an empty buffer.
Returns:
None: This method returns nothing.
"""
self.buffer = GrowwNatsBuffer()
def run(self):
"""Feeds the frame and prints each event.
Returns:
None: This method returns nothing.
"""
frame = 'INFO {"nonce": "example-nonce", "headers": true}\r\nPONG\r\nPING\r\nMSG /ld/eq/nse/book.2885 2 7\r\nab\r\ncde\r\n'
for event in self.buffer.feed(frame):
print(event)
if __name__ == '__main__':
ControlLinesAndAMessageExample().run()
It prints:
"""Feeds a NATS message split across three websocket frames and shows the buffer waiting until it is whole.
A websocket frame may end in the middle of a NATS line or payload, so `GrowwNatsBuffer` keeps what it cannot use yet and hands a message out only once its whole payload has arrived. Bytes and text frames are both accepted.
This program feeds a message in three pieces as bytes, the second ending inside the payload, and then an `-ERR` line as text. It needs no stand-ins.
Notice that the first two feeds return nothing, and that the third returns the whole message.
Run it from the project root:
python examples/stock_brokers/websockets/groww/GrowwNatsBuffer/example_2_split_across_frames.py
"""
from stock_brokers.websockets.groww import (
GrowwNatsBuffer,
)
class SplitAcrossFramesExample:
"""Feeds a split message and an error line to a NATS buffer.
Attributes:
buffer (GrowwNatsBuffer): The buffer being shown.
"""
def __init__(self):
"""Builds an empty buffer.
Returns:
None: This method returns nothing.
"""
self.buffer = GrowwNatsBuffer()
def run(self):
"""Feeds each piece and prints what each feed returned.
Returns:
None: This method returns nothing.
"""
pieces = [
b'MSG stocks/order/upd',
b'ates.apex.subscription-77 1 10\r\n01234',
b'56789\r\n',
"-ERR 'Authorization Violation'\r\n",
]
for piece in pieces:
print(f'Fed {piece!r}: {self.buffer.feed(piece)}')
if __name__ == '__main__':
SplitAcrossFramesExample().run()
It prints:
Methods:
| Name | Description |
|---|---|
feed |
Adds a websocket frame to the stream and takes out everything it completed. |
Source code in stock_brokers/websockets/groww.py
feed
¶
Adds a websocket frame to the stream and takes out everything it completed.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
message
|
bytes | str
|
The frame. |
required |
Returns:
| Type | Description |
|---|---|
list[tuple]
|
In order, |
Example usage
Source code in stock_brokers/websockets/groww.py
GrowwSession
¶
The Groww login every socket authenticates with, logged in again by one socket at a time.
Constructs GrowwAPI, 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
"""Reads the Groww access token, exchanges a key pair for a socket token, and logs in again once.
`GrowwSession` holds one `GrowwAPI` and reads the access token afresh every time a socket connects. `socket_token_payload` asks Groww for a short-lived socket JWT and subscription id bound to a key pair's public key, authorized with that access token, and unwraps the answer's `payload`. `log_in_again` logs in only when the token a refused socket used is still the one in force.
Constructing the real `GrowwAPI` logs in to Groww, so the program registers a stand-in module under `stock_brokers.api.groww`, and a stand-in `requests` package answers the socket token request. The key pair is a real `GrowwNkeyPair`, freshly generated, so the program prints only the first letter of its public key.
Notice the bearer token in the request, the unwrapped payload, and that the second request with the stale token makes no login.
Run it from the project root:
python examples/stock_brokers/websockets/groww/GrowwSession/example_1_socket_token_for_a_key_pair.py
"""
import json
import logging
import sys
import types
from stock_brokers.websockets.groww import (
GrowwNkeyPair,
GrowwSession,
)
class LoginRecord:
"""The Groww 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 StandInGrowwAPI:
"""A stand-in for `GrowwAPI` 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'groww-token-{LoginRecord.login_count}'
print(f'Stand-in login number {LoginRecord.login_count}')
self._settings = {
'api_key': 'groww-api-key',
}
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 SocketTokenResponse:
"""A stand-in for the `requests` response of Groww's socket token endpoint.
Attributes:
status_code (int): The HTTP status.
body (dict): The JSON body.
text (str): The body as text.
"""
def __init__(self, status_code, body):
"""Keeps the answer.
Args:
status_code (int): The HTTP status.
body (dict): The JSON body.
Returns:
None: This method returns nothing.
"""
self.status_code = status_code
self.body = body
self.text = json.dumps(body)
def json(self):
"""The JSON body.
Returns:
dict: The body.
"""
return self.body
def raise_for_status(self):
"""Does nothing, because every scripted answer that reaches it succeeded.
Returns:
None: This method returns nothing.
"""
class StandInRequests:
"""A stand-in for the `requests` package that answers socket token requests in turn.
Attributes:
statuses (list): The HTTP status of each answer, in order.
"""
def __init__(self, statuses):
"""Keeps the scripted statuses.
Args:
statuses (list): The HTTP status of each answer, in order.
Returns:
None: This method returns nothing.
"""
self.statuses = statuses
def post(self, url, headers, json, timeout):
"""Answers one socket token request.
Args:
url (str): The endpoint.
headers (dict): The request headers, carrying the access token.
json (dict): The request body with the public key.
timeout (int): The request timeout in seconds.
Returns:
SocketTokenResponse: The answer.
"""
status = self.statuses.pop(0)
print(f"Socket token request with {headers['Authorization']} for a key starting {json['socketKey'][0]}: HTTP {status}")
if status != 200:
body = {
'message': 'Unauthorized',
}
return SocketTokenResponse(status, body)
body = {
'payload': {
'token': 'socket-jwt',
'subscriptionId': 'subscription-77',
},
}
return SocketTokenResponse(status, body)
class SocketTokenForAKeyPairExample:
"""Asks a Groww session for a socket token and logs in again around it.
Attributes:
session (GrowwSession): The session being shown.
"""
def __init__(self):
"""Registers the stand-ins 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.groww')
api_module.GrowwAPI = StandInGrowwAPI
sys.modules['stock_brokers.api.groww'] = api_module
sys.modules['requests'] = StandInRequests([
200,
])
self.session = GrowwSession(logging.getLogger('groww.session'))
def run(self):
"""Prints the token, the socket token payload, and the token after two login requests.
Returns:
None: This method returns nothing.
"""
failed_token = self.session.access_token()
print(f'Access token: {failed_token}')
payload = self.session.socket_token_payload(GrowwNkeyPair())
print(f'Socket token payload: {payload}')
self.session.log_in_again(failed_token)
self.session.log_in_again(failed_token)
print(f'Access token: {self.session.access_token()}')
print(f'Logins made: {LoginRecord.login_count}')
if __name__ == '__main__':
SocketTokenForAKeyPairExample().run()
It prints:
Stand-in login number 1
Access token: groww-token-1
Socket token request with Bearer groww-token-1 for a key starting U: HTTP 200
Socket token payload: {'token': 'socket-jwt', 'subscriptionId': 'subscription-77'}
WARNING Logging in to Groww again.
Stand-in login number 2
Access token: groww-token-2
Logins made: 2
"""Shows a Groww session's socket token request refused, then a login made without checking and a retried request.
When the socket token endpoint answers HTTP 401 or 403, `GrowwSession.socket_token_payload` raises `GrowwSocketRefused`, because it means the access token is dead. The order update socket answers a refusal with `log_in_again_without_checking`, which logs in whether or not another process already replaced the token, and then asks again.
Constructing the real `GrowwAPI` logs in to Groww, so the program registers a stand-in module under `stock_brokers.api.groww`, and a stand-in `requests` package refuses the first socket token request with HTTP 401 and grants the second.
Notice the refusal message carrying the HTTP status and body, the second login, and that the retried request carries the new token.
Run it from the project root:
python examples/stock_brokers/websockets/groww/GrowwSession/example_2_refused_then_unchecked_login.py
"""
import json
import logging
import sys
import types
from stock_brokers.websockets.groww import (
GrowwNkeyPair,
GrowwSession,
GrowwSocketRefused,
)
class LoginRecord:
"""The Groww 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 StandInGrowwAPI:
"""A stand-in for `GrowwAPI` 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'groww-token-{LoginRecord.login_count}'
print(f'Stand-in login number {LoginRecord.login_count}')
self._settings = {
'api_key': 'groww-api-key',
}
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 SocketTokenResponse:
"""A stand-in for the `requests` response of Groww's socket token endpoint.
Attributes:
status_code (int): The HTTP status.
body (dict): The JSON body.
text (str): The body as text.
"""
def __init__(self, status_code, body):
"""Keeps the answer.
Args:
status_code (int): The HTTP status.
body (dict): The JSON body.
Returns:
None: This method returns nothing.
"""
self.status_code = status_code
self.body = body
self.text = json.dumps(body)
def json(self):
"""The JSON body.
Returns:
dict: The body.
"""
return self.body
def raise_for_status(self):
"""Does nothing, because every scripted answer that reaches it succeeded.
Returns:
None: This method returns nothing.
"""
class StandInRequests:
"""A stand-in for the `requests` package that answers socket token requests in turn.
Attributes:
statuses (list): The HTTP status of each answer, in order.
"""
def __init__(self, statuses):
"""Keeps the scripted statuses.
Args:
statuses (list): The HTTP status of each answer, in order.
Returns:
None: This method returns nothing.
"""
self.statuses = statuses
def post(self, url, headers, json, timeout):
"""Answers one socket token request.
Args:
url (str): The endpoint.
headers (dict): The request headers, carrying the access token.
json (dict): The request body with the public key.
timeout (int): The request timeout in seconds.
Returns:
SocketTokenResponse: The answer.
"""
status = self.statuses.pop(0)
print(f"Socket token request with {headers['Authorization']} for a key starting {json['socketKey'][0]}: HTTP {status}")
if status != 200:
body = {
'message': 'Unauthorized',
}
return SocketTokenResponse(status, body)
body = {
'payload': {
'token': 'socket-jwt',
'subscriptionId': 'subscription-77',
},
}
return SocketTokenResponse(status, body)
class RefusedThenUncheckedLoginExample:
"""Recovers a Groww session from a refused socket token request.
Attributes:
session (GrowwSession): The session being shown.
"""
def __init__(self):
"""Registers the stand-ins 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.groww')
api_module.GrowwAPI = StandInGrowwAPI
sys.modules['stock_brokers.api.groww'] = api_module
sys.modules['requests'] = StandInRequests([
401,
200,
])
self.session = GrowwSession(logging.getLogger('groww.session'))
def run(self):
"""Requests a socket token, logs in again when refused, and requests again.
Returns:
None: This method returns nothing.
"""
key_pair = GrowwNkeyPair()
try:
self.session.socket_token_payload(key_pair)
except GrowwSocketRefused as refusal:
print(f'Refused: {refusal}')
self.session.log_in_again_without_checking()
payload = self.session.socket_token_payload(key_pair)
print(f'Socket token payload: {payload}')
print(f'Logins made: {LoginRecord.login_count}')
if __name__ == '__main__':
RefusedThenUncheckedLoginExample().run()
It prints:
Stand-in login number 1
Socket token request with Bearer groww-token-1 for a key starting U: HTTP 401
Refused: HTTP 401: {"message": "Unauthorized"}
WARNING Logging in to Groww again.
Stand-in login number 2
Socket token request with Bearer groww-token-2 for a key starting U: HTTP 200
Socket token payload: {'token': 'socket-jwt', 'subscriptionId': 'subscription-77'}
Logins made: 2
Methods:
| Name | Description |
|---|---|
access_token |
The access token in force now, which may be one another process has just obtained. |
socket_token_payload |
Asks Groww for a socket JWT bound to a key pair's public key. |
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/groww.py
access_token
¶
The access token in force now, which may be one another process has just obtained.
Returns:
| Type | Description |
|---|---|
str | None
|
The token. |
Example usage
socket_token_payload
¶
Asks Groww for a socket JWT bound to a key pair's public key.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
key_pair
|
GrowwNkeyPair
|
The connection's key pair. |
required |
Returns:
| Type | Description |
|---|---|
dict
|
The answer's payload, which should carry |
Raises:
| Type | Description |
|---|---|
GrowwSocketRefused
|
For HTTP 401 or 403, which means the access token is dead. |
HTTPError
|
For any other error status. |
Example usage
payload = self.session.socket_token_payload(GrowwNkeyPair())
Source code in stock_brokers/websockets/groww.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/groww.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/groww.py
_log_in
¶
Logs in by constructing GrowwAPI 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/groww.py
GrowwQuotesSocket
¶
Bases: BrokerWebsocket
One Groww NATS websocket carrying the price 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; names the payloads reveal are added to it. |
required |
session
|
GrowwSession
|
The shared login. |
required |
on_ticks
|
callable
|
Called with each frame's list of ticks that carry a price or a book, on this socket's thread. |
required |
logger
|
Logger
|
Where the connection reports. |
required |
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Example programs
"""Runs a Groww quotes socket through the socket token, the NATS handshake, and one price and one depth message.
`GrowwQuotesSocket` speaks NATS over a websocket. Before connecting it generates a fresh ed25519 key pair and exchanges its public key for a short-lived socket JWT. When the server opens with an INFO line carrying a nonce, the socket answers CONNECT with the JWT and the signed nonce, then PING, then one SUB for the price subject and one for the depth subject of each instrument. Payloads are protobuf `StocksSocketResponseProtoDto` messages; price and depth arrive separately and are merged per instrument, prices in paise are divided into rupees, and quantities arrive as doubles and are rounded.
This program subscribes NSE token 2885 and delivers a price message and a depth message in one websocket frame. No real socket or HTTP request is made: a stand-in `requests` package answers the socket token request, a stand-in `websocket` package plays the frames and prints the NATS lines the socket sends with the signature replaced by its length, because the key pair is fresh on every run, and a stand-in `stock_brokers.api.groww` module lets the real `GrowwSession` supply the access token without logging in. The payloads are built with `GrowwMessageClasses`.
Notice the two SUB lines, that the first tick is a quote and the second, after the depth message, is full with both, and that the instrument takes the name `NSE:RELIANCE` from the payload's symbol because the program gave it none.
Run it from the project root:
python examples/stock_brokers/websockets/groww/GrowwQuotesSocket/example_1_price_and_depth_over_nats.py
"""
import json
import logging
import sys
import types
from stock_brokers.websockets.groww import (
GrowwMessageClasses,
GrowwQuotesSocket,
GrowwSession,
)
class LoginRecord:
"""The Groww 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 StandInGrowwAPI:
"""A stand-in for `GrowwAPI` 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'groww-token-{LoginRecord.login_count}'
print(f'Stand-in login number {LoginRecord.login_count}')
self._settings = {
'api_key': 'groww-api-key',
}
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 SocketTokenResponse:
"""A stand-in for the `requests` response of Groww's socket token endpoint.
Attributes:
status_code (int): The HTTP status.
body (dict): The JSON body.
text (str): The body as text.
"""
def __init__(self, status_code, body):
"""Keeps the answer.
Args:
status_code (int): The HTTP status.
body (dict): The JSON body.
Returns:
None: This method returns nothing.
"""
self.status_code = status_code
self.body = body
self.text = json.dumps(body)
def json(self):
"""The JSON body.
Returns:
dict: The body.
"""
return self.body
def raise_for_status(self):
"""Does nothing, because every scripted answer that reaches it succeeded.
Returns:
None: This method returns nothing.
"""
class StandInRequests:
"""A stand-in for the `requests` package that answers socket token requests in turn.
Attributes:
statuses (list): The HTTP status of each answer, in order.
"""
def __init__(self, statuses):
"""Keeps the scripted statuses.
Args:
statuses (list): The HTTP status of each answer, in order.
Returns:
None: This method returns nothing.
"""
self.statuses = statuses
def post(self, url, headers, json, timeout):
"""Answers one socket token request.
Args:
url (str): The endpoint.
headers (dict): The request headers, carrying the access token.
json (dict): The request body with the public key.
timeout (int): The request timeout in seconds.
Returns:
SocketTokenResponse: The answer.
"""
status = self.statuses.pop(0)
print(f"Socket token request with {headers['Authorization']} for a key starting {json['socketKey'][0]}: HTTP {status}")
if status != 200:
body = {
'message': 'Unauthorized',
}
return SocketTokenResponse(status, body)
body = {
'payload': {
'token': 'socket-jwt',
'subscriptionId': 'subscription-77',
},
}
return SocketTokenResponse(status, body)
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.
"""
text = data.rstrip('\r\n')
if text.startswith('CONNECT '):
connect = json.loads(text[len('CONNECT '):])
connect['sig'] = f"<{len(connect['sig'])} character signature>"
text = f'CONNECT {json.dumps(connect)}'
print(f'Sent: {text}')
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 NatsFrames:
"""Builds the NATS lines Groww's server sends over the websocket."""
def info(self):
"""The INFO line the server opens with, carrying a nonce.
Returns:
str: The line.
"""
info = {
'server_id': 'groww-nats',
'nonce': 'example-nonce',
'headers': True,
}
return f'INFO {json.dumps(info)}\r\n'
def message(self, subject, payload):
"""A MSG line followed by its payload.
Args:
subject (str): The subject the message arrived on.
payload (bytes): The protobuf payload.
Returns:
bytes: The message.
"""
return f'MSG {subject} 1 {len(payload)}\r\n'.encode() + payload + b'\r\n'
class PriceAndDepthOverNatsExample:
"""Runs a Groww quotes socket against one scripted NATS connection.
Attributes:
socket (GrowwQuotesSocket): The socket being shown.
"""
def __init__(self):
"""Builds the payloads, 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.groww')
api_module.GrowwAPI = StandInGrowwAPI
sys.modules['stock_brokers.api.groww'] = api_module
sys.modules['requests'] = StandInRequests([
200,
])
response_class = GrowwMessageClasses().stocks_response_class()
price = response_class()
price.symbol = 'RELIANCE'
price.stockLivePrice.tsInMillis = 1790311529000
price.stockLivePrice.ltp = 295050
price.stockLivePrice.open = 293000
price.stockLivePrice.high = 296000
price.stockLivePrice.low = 292500
price.stockLivePrice.close = 291000
price.stockLivePrice.volume = 1204500.0
depth = response_class()
depth.symbol = 'RELIANCE'
depth.stocksMarketDepth.buyBook[1].price = 295000
depth.stocksMarketDepth.buyBook[1].qty = 100.0
depth.stocksMarketDepth.sellBook[1].price = 295100
depth.stocksMarketDepth.sellBook[1].qty = 120.0
nats = NatsFrames()
frame = nats.message('/ld/eq/nse/price_detailed.2885', price.SerializeToString())
frame = frame + nats.message('/ld/eq/nse/book.2885', depth.SerializeToString())
plans = [
{
'frames': [
nats.info(),
frame,
],
},
]
websocket_module = ScriptedWebsocketModule(plans)
sys.modules['websocket'] = websocket_module
logger = logging.getLogger('groww.quotes')
tokens = [
'NSE|CASH|2885',
]
self.names = {}
self.socket = GrowwQuotesSocket('socket_0', tokens, self.names, GrowwSession(logger), self.print_ticks, logger)
websocket_module.when_finished = self.socket.close
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} exchange_timestamp={tick['exchange_timestamp']}")
print(f" ohlc={tick['ohlc']}")
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'Names learned: {self.names}')
print(f'Gave up: {self.socket.gave_up}')
if __name__ == '__main__':
PriceAndDepthOverNatsExample().run()
It prints:
Stand-in login number 1
Socket token request with Bearer groww-token-1 for a key starting U: HTTP 200
Connecting to wss://socket-api.groww.in
INFO socket_0 opened. Waiting for the NATS INFO frame.
Sent: CONNECT {"jwt": "socket-jwt", "sig": "<86 character signature>", "verbose": false, "pedantic": false, "tls_required": false, "lang": "python", "version": "1.0.0", "protocol": 1, "headers": true, "no_responders": false}
Sent: PING
Sent: SUB /ld/eq/nse/price_detailed.2885 1
Sent: SUB /ld/eq/nse/book.2885 2
INFO socket_0 subscribed to 2 subject(s) for 1 instrument(s).
Tick NSE:RELIANCE (NSE|CASH|2885) in quote mode
last_price=2950.5 volume=1204500 change=1.3918 exchange_timestamp=1790311529
ohlc={'open': 2930.0, 'high': 2960.0, 'low': 2925.0, 'close': 2910.0}
depth={'buy': [], 'sell': []}
Tick NSE:RELIANCE (NSE|CASH|2885) in full mode
last_price=2950.5 volume=1204500 change=1.3918 exchange_timestamp=1790311529
ohlc={'open': 2930.0, 'high': 2960.0, 'low': 2925.0, 'close': 2910.0}
depth={'buy': [{'quantity': 100, 'price': 2950.0, 'orders': None}], 'sell': [{'quantity': 120, 'price': 2951.0, 'orders': None}]}
INFO socket_0 closed with status 1000: normal closure
INFO socket_0 stopped.
Names learned: {'NSE|CASH|2885': 'NSE:RELIANCE'}
Gave up: False
"""Shows a Groww quotes socket logging in again when its socket token is refused, then answering a server PING.
A dead Groww access token is discovered before any websocket opens: the socket token endpoint answers HTTP 401 or 403. `GrowwQuotesSocket` takes that as a refused login, and its reconnect loop asks the shared `GrowwSession` to log in again, skipping the login when another socket or process already replaced the token. Once connected, the socket answers a NATS `PING` from the server with `PONG`.
This program's first socket token request is refused with HTTP 403 and the second succeeds. The second connection opens with INFO, then the server sends a PING and an index value for the NIFTY index (token 26000 on the NSE cash segment). No real socket or HTTP request is made: stand-in `requests` and `websocket` packages play the answers and frames, and a stand-in `stock_brokers.api.groww` module counts each `GrowwAPI` construction as one login. The backoff is set to zero seconds.
Notice that the second token request carries the new access token, the PONG line, and that an index tick has a last price but no open, high, low or close.
Run it from the project root:
python examples/stock_brokers/websockets/groww/GrowwQuotesSocket/example_2_refused_socket_token.py
"""
import json
import logging
import sys
import types
from stock_brokers.websockets.groww import (
GrowwMessageClasses,
GrowwQuotesSocket,
GrowwSession,
)
class LoginRecord:
"""The Groww 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 StandInGrowwAPI:
"""A stand-in for `GrowwAPI` 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'groww-token-{LoginRecord.login_count}'
print(f'Stand-in login number {LoginRecord.login_count}')
self._settings = {
'api_key': 'groww-api-key',
}
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 SocketTokenResponse:
"""A stand-in for the `requests` response of Groww's socket token endpoint.
Attributes:
status_code (int): The HTTP status.
body (dict): The JSON body.
text (str): The body as text.
"""
def __init__(self, status_code, body):
"""Keeps the answer.
Args:
status_code (int): The HTTP status.
body (dict): The JSON body.
Returns:
None: This method returns nothing.
"""
self.status_code = status_code
self.body = body
self.text = json.dumps(body)
def json(self):
"""The JSON body.
Returns:
dict: The body.
"""
return self.body
def raise_for_status(self):
"""Does nothing, because every scripted answer that reaches it succeeded.
Returns:
None: This method returns nothing.
"""
class StandInRequests:
"""A stand-in for the `requests` package that answers socket token requests in turn.
Attributes:
statuses (list): The HTTP status of each answer, in order.
"""
def __init__(self, statuses):
"""Keeps the scripted statuses.
Args:
statuses (list): The HTTP status of each answer, in order.
Returns:
None: This method returns nothing.
"""
self.statuses = statuses
def post(self, url, headers, json, timeout):
"""Answers one socket token request.
Args:
url (str): The endpoint.
headers (dict): The request headers, carrying the access token.
json (dict): The request body with the public key.
timeout (int): The request timeout in seconds.
Returns:
SocketTokenResponse: The answer.
"""
status = self.statuses.pop(0)
print(f"Socket token request with {headers['Authorization']} for a key starting {json['socketKey'][0]}: HTTP {status}")
if status != 200:
body = {
'message': 'Unauthorized',
}
return SocketTokenResponse(status, body)
body = {
'payload': {
'token': 'socket-jwt',
'subscriptionId': 'subscription-77',
},
}
return SocketTokenResponse(status, body)
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.
"""
text = data.rstrip('\r\n')
if text.startswith('CONNECT '):
connect = json.loads(text[len('CONNECT '):])
connect['sig'] = f"<{len(connect['sig'])} character signature>"
text = f'CONNECT {json.dumps(connect)}'
print(f'Sent: {text}')
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 NatsFrames:
"""Builds the NATS lines Groww's server sends over the websocket."""
def info(self):
"""The INFO line the server opens with, carrying a nonce.
Returns:
str: The line.
"""
info = {
'server_id': 'groww-nats',
'nonce': 'example-nonce',
'headers': True,
}
return f'INFO {json.dumps(info)}\r\n'
def message(self, subject, payload):
"""A MSG line followed by its payload.
Args:
subject (str): The subject the message arrived on.
payload (bytes): The protobuf payload.
Returns:
bytes: The message.
"""
return f'MSG {subject} 1 {len(payload)}\r\n'.encode() + payload + b'\r\n'
class RefusedSocketTokenExample:
"""Runs a Groww quotes socket through a refused token and a good connection.
Attributes:
socket (GrowwQuotesSocket): The socket being shown.
"""
def __init__(self):
"""Builds the payload, 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.groww')
api_module.GrowwAPI = StandInGrowwAPI
sys.modules['stock_brokers.api.groww'] = api_module
sys.modules['requests'] = StandInRequests([
403,
200,
])
index = GrowwMessageClasses().stocks_response_class()()
index.stocksLiveIndices.value = 2515065
index.stocksLiveIndices.tsInMillis = 1790311529000
nats = NatsFrames()
plans = [
{
'frames': [
nats.info(),
'PING\r\n',
nats.message('/ld/eq/nse/price_detailed.26000', index.SerializeToString()),
],
},
]
websocket_module = ScriptedWebsocketModule(plans)
sys.modules['websocket'] = websocket_module
logger = logging.getLogger('groww.quotes')
tokens = [
'NSE|CASH|26000',
]
names = {
'NSE|CASH|26000': 'NSE:NIFTY',
}
self.socket = GrowwQuotesSocket('socket_0', tokens, names, GrowwSession(logger), self.print_ticks, logger)
self.socket.MIN_BACKOFF_SECONDS = 0
websocket_module.when_finished = self.socket.close
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} exchange_timestamp={tick['exchange_timestamp']}")
print(f" ohlc={tick['ohlc']}")
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}')
print(f'Logins made: {LoginRecord.login_count}')
if __name__ == '__main__':
RefusedSocketTokenExample().run()
It prints:
Stand-in login number 1
Socket token request with Bearer groww-token-1 for a key starting U: HTTP 403
ERROR socket_0 socket token refused: HTTP 403: {"message": "Unauthorized"}
WARNING Logging in to Groww again.
Stand-in login number 2
WARNING socket_0 disconnected. Reconnecting in 0 seconds.
Socket token request with Bearer groww-token-2 for a key starting U: HTTP 200
Connecting to wss://socket-api.groww.in
INFO socket_0 opened. Waiting for the NATS INFO frame.
Sent: CONNECT {"jwt": "socket-jwt", "sig": "<86 character signature>", "verbose": false, "pedantic": false, "tls_required": false, "lang": "python", "version": "1.0.0", "protocol": 1, "headers": true, "no_responders": false}
Sent: PING
Sent: SUB /ld/eq/nse/price_detailed.26000 1
Sent: SUB /ld/eq/nse/book.26000 2
INFO socket_0 subscribed to 2 subject(s) for 1 instrument(s).
Sent: PONG
Tick NSE:NIFTY (NSE|CASH|26000) in quote mode
last_price=25150.65 volume=None change=None exchange_timestamp=1790311529
ohlc={'open': None, 'high': None, 'low': None, 'close': None}
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/groww.py
_gives_up_after_logging_in_again
¶
Gives up only once six connects in a row have failed, as Groww's quotes loop always has.
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/groww.py
_connect
¶
Obtains a socket JWT for a fresh key pair, opens the websocket, and blocks until it closes.
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
When the socket token answer carries no token. |
Source code in stock_brokers/websockets/groww.py
_log_in_again
¶
Logs in again through the shared session, which skips the login when the token was already replaced.
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
_on_open
¶
Notes the open connection; NATS speaks first, with INFO.
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/groww.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/groww.py
_send
¶
Sends NATS lines, one websocket frame each.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lines
|
list[str]
|
The lines. |
required |
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Source code in stock_brokers/websockets/groww.py
_on_message
¶
Consumes a frame as NATS, answers its control lines, and hands on the ticks its messages completed.
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/groww.py
_authenticate
¶
Answers INFO with CONNECT and the signed nonce, then subscribes the batch's price and depth subjects.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
info
|
dict
|
The decoded INFO frame. |
required |
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Source code in stock_brokers/websockets/groww.py
_on_nats_error
¶
Reports a NATS error, and closes the connection when it is an authorization failure.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
error
|
str
|
The |
required |
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Source code in stock_brokers/websockets/groww.py
_apply_payload
¶
Merges one protobuf payload into an instrument's state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token
|
str
|
The instrument's |
required |
payload
|
bytes
|
The protobuf bytes. |
required |
Returns:
| Type | Description |
|---|---|
dict
|
The instrument's merged state. |
Raises:
| Type | Description |
|---|---|
DecodeError
|
When the payload is not a |
Source code in stock_brokers/websockets/groww.py
_levels
¶
Depth levels from a protobuf book map keyed by level, read in level order.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
book_map
|
MessageMap
|
The buy or sell book. |
required |
Returns:
| Type | Description |
|---|---|
list[dict]
|
The levels. |
Source code in stock_brokers/websockets/groww.py
_price
¶
A price from the merged state in rupees.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
dict
|
The merged state. |
required |
key
|
str
|
The field. |
required |
Returns:
| Type | Description |
|---|---|
float | None
|
The price, or None when the field is missing. |
Source code in stock_brokers/websockets/groww.py
_whole
¶
A protobuf double quantity as a whole number.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
float | None
|
The quantity. |
required |
Returns:
| Type | Description |
|---|---|
int | None
|
The rounded quantity, or None. |
Source code in stock_brokers/websockets/groww.py
_build_tick
¶
Builds a normalized tick from an instrument's merged state.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
token
|
str
|
The instrument's |
required |
state
|
dict
|
The merged fields. |
required |
Returns:
| Type | Description |
|---|---|
dict
|
The tick. |
Source code in stock_brokers/websockets/groww.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
_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
GrowwOrderUpdatesSocket
¶
Bases: BrokerWebsocket
Groww's order and position update stream for the account, over NATS.
Sets up the order updates socket.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
session
|
GrowwSession
|
The login. |
required |
on_updates
|
callable
|
Called on this socket's thread with a frame's decoded orders, its decoded positions and the epoch the frame was received. |
required |
logger
|
Logger
|
Where the connection reports. |
required |
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Example programs
"""Receives protobuf orders and positions on Groww's order update stream and prints the decoded dictionaries.
`GrowwOrderUpdatesSocket` obtains a socket JWT and a subscription id, answers the server's INFO with CONNECT, and subscribes the account's equity orders, derivatives orders and derivatives positions subjects, each keyed by the subscription id. Each payload is decoded to a plain dictionary with Groww's own field names, enum fields by name and every field present even when zero.
This program delivers an equity order, a derivatives position and a message on a subject the socket never subscribed, split across two websocket frames. No real socket or HTTP request is made: a stand-in `requests` package answers the socket token request, a stand-in `websocket` package plays the frames and prints the NATS lines the socket sends with the signature replaced by its length, and a stand-in `stock_brokers.api.groww` module lets the real `GrowwSession` supply the access token. The payloads are built with `GrowwMessageClasses`.
Notice the three SUB lines ending in `subscription-77`, that enums such as `orderStatus` arrive by name, and that the unexpected subject is logged and skipped.
Run it from the project root:
python examples/stock_brokers/websockets/groww/GrowwOrderUpdatesSocket/example_1_orders_and_positions.py
"""
import json
import logging
import sys
import types
from stock_brokers.websockets.groww import (
GrowwMessageClasses,
GrowwOrderUpdatesSocket,
GrowwSession,
)
class LoginRecord:
"""The Groww 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 StandInGrowwAPI:
"""A stand-in for `GrowwAPI` 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'groww-token-{LoginRecord.login_count}'
print(f'Stand-in login number {LoginRecord.login_count}')
self._settings = {
'api_key': 'groww-api-key',
}
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 SocketTokenResponse:
"""A stand-in for the `requests` response of Groww's socket token endpoint.
Attributes:
status_code (int): The HTTP status.
body (dict): The JSON body.
text (str): The body as text.
"""
def __init__(self, status_code, body):
"""Keeps the answer.
Args:
status_code (int): The HTTP status.
body (dict): The JSON body.
Returns:
None: This method returns nothing.
"""
self.status_code = status_code
self.body = body
self.text = json.dumps(body)
def json(self):
"""The JSON body.
Returns:
dict: The body.
"""
return self.body
def raise_for_status(self):
"""Does nothing, because every scripted answer that reaches it succeeded.
Returns:
None: This method returns nothing.
"""
class StandInRequests:
"""A stand-in for the `requests` package that answers socket token requests in turn.
Attributes:
statuses (list): The HTTP status of each answer, in order.
"""
def __init__(self, statuses):
"""Keeps the scripted statuses.
Args:
statuses (list): The HTTP status of each answer, in order.
Returns:
None: This method returns nothing.
"""
self.statuses = statuses
def post(self, url, headers, json, timeout):
"""Answers one socket token request.
Args:
url (str): The endpoint.
headers (dict): The request headers, carrying the access token.
json (dict): The request body with the public key.
timeout (int): The request timeout in seconds.
Returns:
SocketTokenResponse: The answer.
"""
status = self.statuses.pop(0)
print(f"Socket token request with {headers['Authorization']} for a key starting {json['socketKey'][0]}: HTTP {status}")
if status != 200:
body = {
'message': 'Unauthorized',
}
return SocketTokenResponse(status, body)
body = {
'payload': {
'token': 'socket-jwt',
'subscriptionId': 'subscription-77',
},
}
return SocketTokenResponse(status, body)
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.
"""
text = data.rstrip('\r\n')
if text.startswith('CONNECT '):
connect = json.loads(text[len('CONNECT '):])
connect['sig'] = f"<{len(connect['sig'])} character signature>"
text = f'CONNECT {json.dumps(connect)}'
print(f'Sent: {text}')
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 NatsFrames:
"""Builds the NATS lines Groww's server sends over the websocket."""
def info(self):
"""The INFO line the server opens with, carrying a nonce.
Returns:
str: The line.
"""
info = {
'server_id': 'groww-nats',
'nonce': 'example-nonce',
'headers': True,
}
return f'INFO {json.dumps(info)}\r\n'
def message(self, subject, payload):
"""A MSG line followed by its payload.
Args:
subject (str): The subject the message arrived on.
payload (bytes): The protobuf payload.
Returns:
bytes: The message.
"""
return f'MSG {subject} 1 {len(payload)}\r\n'.encode() + payload + b'\r\n'
class OrdersAndPositionsExample:
"""Runs the Groww order update socket against one scripted NATS connection.
Attributes:
socket (GrowwOrderUpdatesSocket): The socket being shown.
"""
def __init__(self):
"""Builds the payloads, 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.groww')
api_module.GrowwAPI = StandInGrowwAPI
sys.modules['stock_brokers.api.groww'] = api_module
sys.modules['requests'] = StandInRequests([
200,
])
position_class = GrowwMessageClasses().order_and_position_classes()[1]
position = position_class()
position.symbolData.contractId = 'NIFTY26SEP25000CE'
position.symbolData.displayName = 'NIFTY 25000 Call 30 Sep'
position.positionInfo.NSE.creditQty = 75
position.positionInfo.NSE.creditPrice = 12550
nats = NatsFrames()
whole = nats.message('stocks/order/updates.apex.subscription-77', self.order_payload(11, 10))
whole = whole + nats.message('stocks_fo/position/updates.apex.subscription-77', position.SerializeToString())
stray = nats.message('stocks/order/updates.apex.someone-else', self.order_payload(1, 0))
plans = [
{
'frames': [
nats.info(),
whole[:30],
whole[30:] + stray,
],
},
]
websocket_module = ScriptedWebsocketModule(plans)
sys.modules['websocket'] = websocket_module
logger = logging.getLogger('groww.orders')
self.socket = GrowwOrderUpdatesSocket(GrowwSession(logger), self.print_updates, logger)
websocket_module.when_finished = self.socket.close
def order_payload(self, status, filled_quantity):
"""A serialized `OrderDetailsBroadCastDto` for an INFY buy.
Args:
status (int): Groww's order status number, 1 for acknowledged and 11 for completed.
filled_quantity (int): How much has filled.
Returns:
bytes: The protobuf payload.
"""
order_class = GrowwMessageClasses().order_and_position_classes()[0]
broadcast = order_class()
detail = broadcast.orderDetailUpdateDto
detail.growwOrderId = 'GMK2609250001'
detail.qty = 10
detail.filledQty = filled_quantity
detail.price = 150000
detail.orderStatus = status
detail.exchange = 1
detail.buySell = 0
return broadcast.SerializeToString()
def print_updates(self, orders, positions, received_at):
"""Prints each decoded order and position the socket handed on.
Args:
orders (list): The frame's orders, as dictionaries with Groww's field names.
positions (list): The frame's positions, as dictionaries with Groww's field names.
received_at (float): The epoch the frame arrived, which is not printed.
Returns:
None: This method returns nothing.
"""
for order in orders:
detail = order['orderDetailUpdateDto']
print(f"Order {detail['growwOrderId']} {detail['orderStatus']}: {detail['buySell']} {detail['filledQty']} of {detail['qty']} on {detail['exchange']} at {detail['price']}")
for position in positions:
print(f"Position {position['symbolData']['contractId']}: NSE credit quantity {position['positionInfo']['NSE']['creditQty']}")
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__':
OrdersAndPositionsExample().run()
It prints:
Stand-in login number 1
Socket token request with Bearer groww-token-1 for a key starting U: HTTP 200
Connecting to wss://socket-api.groww.in
INFO Order updates socket opened. Waiting for the NATS INFO frame.
Sent: CONNECT {"jwt": "socket-jwt", "sig": "<86 character signature>", "verbose": false, "pedantic": false, "tls_required": false, "lang": "python", "version": "1.0.0", "protocol": 1, "headers": true, "no_responders": false}
Sent: PING
Sent: SUB stocks/order/updates.apex.subscription-77 1
Sent: SUB stocks_fo/order/updates.apex.subscription-77 2
Sent: SUB stocks_fo/position/updates.apex.subscription-77 3
INFO Authenticated and subscribed to 3 subject(s). Waiting for updates.
INFO Ignoring a message on an unexpected subject stocks/order/updates.apex.someone-else.
Order GMK2609250001 COMPLETED: B 10 of 10 on NSE at 150000
Position NIFTY26SEP25000CE: NSE credit quantity 75.0
INFO Order updates socket closed with status 1000: normal closure
INFO Order updates socket stopped.
Gave up: False
"""Shows Groww's order update socket closing on a NATS authorization error and logging in again without checking.
When the NATS server answers with an `-ERR` naming an authorization failure, `GrowwOrderUpdatesSocket` closes the connection as a refused login, and its reconnect loop calls `GrowwSession.log_in_again_without_checking`, which logs in whether or not another process already replaced the token. The next connection asks for a new socket JWT with a new key pair.
This program's first connection answers CONNECT with `-ERR 'Authorization Violation'`; the second delivers one acknowledged order. No real socket or HTTP request is made: stand-in `requests` and `websocket` packages play the answers and frames, and a stand-in `stock_brokers.api.groww` module counts each `GrowwAPI` construction as one login. The backoff is set to zero seconds.
Notice the NATS error line, the second login, and that the second socket token request carries the new access token.
Run it from the project root:
python examples/stock_brokers/websockets/groww/GrowwOrderUpdatesSocket/example_2_authorization_violation.py
"""
import json
import logging
import sys
import types
from stock_brokers.websockets.groww import (
GrowwMessageClasses,
GrowwOrderUpdatesSocket,
GrowwSession,
)
class LoginRecord:
"""The Groww 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 StandInGrowwAPI:
"""A stand-in for `GrowwAPI` 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'groww-token-{LoginRecord.login_count}'
print(f'Stand-in login number {LoginRecord.login_count}')
self._settings = {
'api_key': 'groww-api-key',
}
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 SocketTokenResponse:
"""A stand-in for the `requests` response of Groww's socket token endpoint.
Attributes:
status_code (int): The HTTP status.
body (dict): The JSON body.
text (str): The body as text.
"""
def __init__(self, status_code, body):
"""Keeps the answer.
Args:
status_code (int): The HTTP status.
body (dict): The JSON body.
Returns:
None: This method returns nothing.
"""
self.status_code = status_code
self.body = body
self.text = json.dumps(body)
def json(self):
"""The JSON body.
Returns:
dict: The body.
"""
return self.body
def raise_for_status(self):
"""Does nothing, because every scripted answer that reaches it succeeded.
Returns:
None: This method returns nothing.
"""
class StandInRequests:
"""A stand-in for the `requests` package that answers socket token requests in turn.
Attributes:
statuses (list): The HTTP status of each answer, in order.
"""
def __init__(self, statuses):
"""Keeps the scripted statuses.
Args:
statuses (list): The HTTP status of each answer, in order.
Returns:
None: This method returns nothing.
"""
self.statuses = statuses
def post(self, url, headers, json, timeout):
"""Answers one socket token request.
Args:
url (str): The endpoint.
headers (dict): The request headers, carrying the access token.
json (dict): The request body with the public key.
timeout (int): The request timeout in seconds.
Returns:
SocketTokenResponse: The answer.
"""
status = self.statuses.pop(0)
print(f"Socket token request with {headers['Authorization']} for a key starting {json['socketKey'][0]}: HTTP {status}")
if status != 200:
body = {
'message': 'Unauthorized',
}
return SocketTokenResponse(status, body)
body = {
'payload': {
'token': 'socket-jwt',
'subscriptionId': 'subscription-77',
},
}
return SocketTokenResponse(status, body)
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.
"""
text = data.rstrip('\r\n')
if text.startswith('CONNECT '):
connect = json.loads(text[len('CONNECT '):])
connect['sig'] = f"<{len(connect['sig'])} character signature>"
text = f'CONNECT {json.dumps(connect)}'
print(f'Sent: {text}')
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 NatsFrames:
"""Builds the NATS lines Groww's server sends over the websocket."""
def info(self):
"""The INFO line the server opens with, carrying a nonce.
Returns:
str: The line.
"""
info = {
'server_id': 'groww-nats',
'nonce': 'example-nonce',
'headers': True,
}
return f'INFO {json.dumps(info)}\r\n'
def message(self, subject, payload):
"""A MSG line followed by its payload.
Args:
subject (str): The subject the message arrived on.
payload (bytes): The protobuf payload.
Returns:
bytes: The message.
"""
return f'MSG {subject} 1 {len(payload)}\r\n'.encode() + payload + b'\r\n'
class AuthorizationViolationExample:
"""Runs the Groww order update socket through an authorization error and a good connection.
Attributes:
socket (GrowwOrderUpdatesSocket): The socket being shown.
"""
def __init__(self):
"""Builds the payloads, 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.groww')
api_module.GrowwAPI = StandInGrowwAPI
sys.modules['stock_brokers.api.groww'] = api_module
sys.modules['requests'] = StandInRequests([
200,
200,
])
nats = NatsFrames()
plans = [
{
'frames': [
nats.info(),
"-ERR 'Authorization Violation'\r\n",
],
},
{
'frames': [
nats.info(),
nats.message('stocks/order/updates.apex.subscription-77', self.order_payload(1, 0)),
],
},
]
websocket_module = ScriptedWebsocketModule(plans)
sys.modules['websocket'] = websocket_module
logger = logging.getLogger('groww.orders')
self.socket = GrowwOrderUpdatesSocket(GrowwSession(logger), self.print_updates, logger)
self.socket.MIN_BACKOFF_SECONDS = 0
websocket_module.when_finished = self.socket.close
def order_payload(self, status, filled_quantity):
"""A serialized `OrderDetailsBroadCastDto` for an INFY buy.
Args:
status (int): Groww's order status number, 1 for acknowledged and 11 for completed.
filled_quantity (int): How much has filled.
Returns:
bytes: The protobuf payload.
"""
order_class = GrowwMessageClasses().order_and_position_classes()[0]
broadcast = order_class()
detail = broadcast.orderDetailUpdateDto
detail.growwOrderId = 'GMK2609250001'
detail.qty = 10
detail.filledQty = filled_quantity
detail.price = 150000
detail.orderStatus = status
detail.exchange = 1
detail.buySell = 0
return broadcast.SerializeToString()
def print_updates(self, orders, positions, received_at):
"""Prints each decoded order and position the socket handed on.
Args:
orders (list): The frame's orders, as dictionaries with Groww's field names.
positions (list): The frame's positions, as dictionaries with Groww's field names.
received_at (float): The epoch the frame arrived, which is not printed.
Returns:
None: This method returns nothing.
"""
for order in orders:
detail = order['orderDetailUpdateDto']
print(f"Order {detail['growwOrderId']} {detail['orderStatus']}: {detail['buySell']} {detail['filledQty']} of {detail['qty']} on {detail['exchange']} at {detail['price']}")
for position in positions:
print(f"Position {position['symbolData']['contractId']}: NSE credit quantity {position['positionInfo']['NSE']['creditQty']}")
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__':
AuthorizationViolationExample().run()
It prints:
Stand-in login number 1
Socket token request with Bearer groww-token-1 for a key starting U: HTTP 200
Connecting to wss://socket-api.groww.in
INFO Order updates socket opened. Waiting for the NATS INFO frame.
Sent: CONNECT {"jwt": "socket-jwt", "sig": "<86 character signature>", "verbose": false, "pedantic": false, "tls_required": false, "lang": "python", "version": "1.0.0", "protocol": 1, "headers": true, "no_responders": false}
Sent: PING
Sent: SUB stocks/order/updates.apex.subscription-77 1
Sent: SUB stocks_fo/order/updates.apex.subscription-77 2
Sent: SUB stocks_fo/position/updates.apex.subscription-77 3
INFO Authenticated and subscribed to 3 subject(s). Waiting for updates.
ERROR NATS error: -ERR 'Authorization Violation'
INFO Order updates socket closed with status 1000: normal closure
WARNING Logging in to Groww again.
Stand-in login number 2
WARNING Order updates socket disconnected. Reconnecting in 0 seconds.
Socket token request with Bearer groww-token-2 for a key starting U: HTTP 200
Connecting to wss://socket-api.groww.in
INFO Order updates socket opened. Waiting for the NATS INFO frame.
Sent: CONNECT {"jwt": "socket-jwt", "sig": "<86 character signature>", "verbose": false, "pedantic": false, "tls_required": false, "lang": "python", "version": "1.0.0", "protocol": 1, "headers": true, "no_responders": false}
Sent: PING
Sent: SUB stocks/order/updates.apex.subscription-77 1
Sent: SUB stocks_fo/order/updates.apex.subscription-77 2
Sent: SUB stocks_fo/position/updates.apex.subscription-77 3
INFO Authenticated and subscribed to 3 subject(s). Waiting for updates.
Order GMK2609250001 ACKED: B 0 of 10 on NSE at 150000
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/groww.py
_connect
¶
Obtains a socket JWT and subscription id for a fresh key pair, opens the websocket, and blocks until it closes.
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
When the socket token answer carries no token or no subscription id. |
Source code in stock_brokers/websockets/groww.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
¶
Notes the open connection; NATS speaks first, with INFO.
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/groww.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/groww.py
_order_subjects
¶
The subjects carrying this account's equity and derivatives orders.
Returns:
| Type | Description |
|---|---|
list[str]
|
The subjects. |
Source code in stock_brokers/websockets/groww.py
_position_subjects
¶
The subject carrying this account's derivatives positions.
Returns:
| Type | Description |
|---|---|
list[str]
|
The subjects. |
_on_message
¶
Consumes a frame as NATS, answers its control lines, and hands on the orders and positions its messages carried.
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/groww.py
_authenticate
¶
Answers INFO with CONNECT and the signed nonce, then subscribes the account's order and position subjects.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
info
|
dict
|
The decoded INFO frame. |
required |
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Source code in stock_brokers/websockets/groww.py
_on_nats_error
¶
Reports a NATS error, and closes the connection when it is an authorization failure.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
error
|
str
|
The |
required |
Returns:
| Type | Description |
|---|---|
None
|
This method returns nothing. |
Source code in stock_brokers/websockets/groww.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. |