Skip to content

wisdom_capital

Wisdom Capital's two XTS websockets: the market data feed and the interactive (trading) namespace that pushes order and position events.

Two applications, two sessions. XTS splits a broker into a market data application and an interactive one. Quotes need the market data session, whose token is minted from price_api_key and price_api_secret; XTS issues exactly one per application key, and a second login invalidates the first. WisdomCapitalMarketDataSession reads the market data token from the shared login and asks WisdomCapitalAPI to replace it when it is refused, which the API does under a Redis lock, and only when nobody has replaced it already. Orders and positions live in the interactive application, which also keeps one session per key: WisdomCapitalInteractiveSession joins with the token WisdomCapitalAPI shares through last_login, reads the user id from that token's JWT payload, and never logs in for itself except by constructing WisdomCapitalAPI again, which keeps a stored token that still works.

The transport. The host's certificate is issued to the platform provider's domain, so the exact certificate is pinned instead of the hostname being checked: HTTPS requests go through a urllib3 pool that asserts the fingerprint, and a websocket's certificate is compared with the pin as soon as it opens. A connection opens an Engine.IO session over HTTPS polling, attaches a websocket with the session id, sends the 2probe probe, answers 3probe with 5 to complete the upgrade, and then pings (2) at half the shorter of the ping interval and timeout the server asked for, since the two disagree and the interactive namespace waits only twenty seconds. A server ping is answered with 3. XTS reports a refused token with e-session, e-token or "Invalid Token" in the answer.

Quotes. WisdomCapitalQuotesSocket attaches with publishFormat=JSON&broadcastMode=Full and subscribes over REST, for touchline (1501) and market depth (1502); the answer carries the current snapshot, which is handed on straight away. XTS reports instruments still subscribed from an earlier connection as e-session-0002 Instrument Already Subscribed, with the same prefix as a refused token, so that answer is met by dropping the subscription and asking again rather than by replacing a good token. Events arrive as socket.io frames 42["<event>", <payload>], the payload a quote, a list of them, or either as a JSON string; touchline and depth for one instrument are merged, a depth message nesting its touchline fields, before a tick is built. XTS counts seconds from 1980-01-01 in India time, so its timestamps are moved onto the true Unix epoch, and it misspells the last traded quantity as LastTradedQunatity, sending the key with a null value. A tick with neither a price nor a book is not handed on.

Order updates. WisdomCapitalOrderUpdatesSocket joins the interactive namespace with apiType=INTERACTIVE, which is enough for XTS to push every order, trade and position event for the account; without it the server cuts the connection after ten seconds. When another login replaces the token, XTS sends a logout event and leaves the connection open while delivering nothing more, so the socket closes, waits up to ten seconds for the replacing login to reach Redis, and reconnects with it; this wait is why the socket keeps a reconnect loop of its own. A trade event is not handed on, because the order event that follows carries the fill.

Neither socket writes Redis. The quotes socket hands ticks to on_ticks, and the order socket hands each event's orders or positions to on_updates; the scripts in bin/wisdom_capital/ do the writing.

Classes:

Name Description
WisdomCapitalTransport

The pinned HTTPS requests, the Engine.IO handshake and the heartbeat both XTS sockets use.

WisdomCapitalMarketDataSession

The market data login every quotes socket authenticates with.

WisdomCapitalInteractiveSession

The interactive login the order updates socket joins with, shared with every Wisdom Capital process.

WisdomCapitalQuotesSocket

One XTS market data socket carrying the touchline and depth of a batch of instruments.

WisdomCapitalOrderUpdatesSocket

Wisdom Capital's XTS interactive socket, which carries the account's order and position events.

WisdomCapitalTransport

The pinned HTTPS requests, the Engine.IO handshake and the heartbeat both XTS sockets use.

Example programs

examples/stock_brokers/websockets/wisdom_capital/WisdomCapitalTransport/example_1_engine_io_handshake.py
"""Opens an XTS Engine.IO session through `WisdomCapitalTransport` and derives the websocket URL, heartbeat and TLS options from it.

`WisdomCapitalTransport` holds what both Wisdom Capital sockets need to reach XTS. `request` sends an HTTPS request through a urllib3 pool pinned to the platform's certificate, because the certificate is issued to the platform provider's domain rather than Wisdom Capital's. `handshake` opens an Engine.IO session over polling and decodes the answer with `first_json_object`, since the JSON is framed with a length prefix. `heartbeat_seconds` picks half the shorter of the server's ping interval and timeout, `websocket_url` attaches a websocket to the session, and `ssl_options` relaxes the hostname check because the pin is checked once the websocket opens.

This program performs one handshake and one plain request against a stand-in `urllib3` package, whose pool prints each request and gives a scripted answer, and reads a polling body with no JSON in it. No network is used.

Notice that the heartbeat is 10 seconds, half of the 20 second ping timeout, and that `check_hostname` is off while certificate verification stays required.

Run it from the project root:

    python examples/stock_brokers/websockets/wisdom_capital/WisdomCapitalTransport/example_1_engine_io_handshake.py
"""

import json
import ssl
import sys

from stock_brokers.websockets.wisdom_capital import (
    WisdomCapitalTransport,
)


class PinnedResponse:
    """A stand-in for the `urllib3` response of one XTS request.

    Attributes:
        status (int): The HTTP status.
        data (bytes): The body.
    """

    def __init__(self, status, text):
        """Keeps the answer.

        Args:
            status (int): The HTTP status.
            text (str): The body as text.

        Returns:
            None: This method returns nothing.
        """
        self.status = status
        self.data = text.encode()


class PinnedPool:
    """A stand-in for `urllib3.HTTPSConnectionPool` that answers from a shared script.

    Attributes:
        answers (list): The answers still to give, shared by every pool.
    """

    def __init__(self, answers):
        """Keeps the shared answers.

        Args:
            answers (list): The answers still to give, shared by every pool.

        Returns:
            None: This method returns nothing.
        """
        self.answers = answers

    def request(self, method, path, body=None, headers=None, timeout=None):
        """Prints the request and gives the next scripted answer.

        Args:
            method (str): The HTTP method.
            path (str): The path, with its query string.
            body (str | None): The JSON body.
            headers (dict | None): The request headers.
            timeout (int | None): The request timeout in seconds.

        Returns:
            PinnedResponse: The answer.
        """
        shown = path.split('?')[0]
        if body is not None:
            code = json.loads(body).get('xtsMessageCode')
            shown = f'{shown} for message code {code}'
        answer = self.answers.pop(0)
        print(f'HTTPS {method} {shown}: HTTP {answer.status}')
        return answer


class StandInUrllib3:
    """A stand-in for the `urllib3` package whose pools answer from one script.

    Attributes:
        answers (list): The answers, in the order requests are made.
    """

    def __init__(self, answers):
        """Keeps the answers.

        Args:
            answers (list): The answers, in the order requests are made.

        Returns:
            None: This method returns nothing.
        """
        self.answers = answers

    def HTTPSConnectionPool(self, host, port, assert_fingerprint=None):
        """Builds a pool, with the same signature as the real class.

        Args:
            host (str): The host.
            port (int): The port.
            assert_fingerprint (str | None): The certificate pin.

        Returns:
            PinnedPool: The pool.
        """
        return PinnedPool(self.answers)


class EngineIoHandshakeExample:
    """Performs a handshake and derives the connection details from it.

    Attributes:
        transport (WisdomCapitalTransport): The transport being shown.
    """

    def __init__(self):
        """Registers the stand-in urllib3 package and builds the transport.

        Returns:
            None: This method returns nothing.
        """
        handshake = {
            'sid': 'engine-session-1',
            'upgrades': [
                'websocket',
            ],
            'pingInterval': 25000,
            'pingTimeout': 20000,
        }
        text = json.dumps(handshake)
        success = {
            'type': 'success',
        }
        answers = [
            PinnedResponse(200, f'{len(text) + 1}:0{text}2:40'),
            PinnedResponse(200, json.dumps(success)),
        ]
        sys.modules['urllib3'] = StandInUrllib3(answers)
        self.transport = WisdomCapitalTransport()

    def run(self):
        """Prints the handshake and everything derived from it.

        Returns:
            None: This method returns nothing.
        """
        query = 'token=market-data-token-1&userID=WCMD01&publishFormat=JSON&broadcastMode=Full'
        handshake, status, body = self.transport.handshake('/apimarketdata', query)
        print(f'Handshake {status}: {handshake}')
        print(f'Heartbeat every {self.transport.heartbeat_seconds(handshake, 25.0)} seconds')
        print(f'Heartbeat without limits from the server: {self.transport.heartbeat_seconds({}, 25.0)} seconds')
        print(f"Websocket URL: {self.transport.websocket_url('/apimarketdata', handshake, query)}")
        context = self.transport.ssl_options()['context']
        print(f'check_hostname={context.check_hostname} verify_mode={context.verify_mode == ssl.CERT_REQUIRED}')
        subscription = {
            'xtsMessageCode': 1501,
        }
        response = self.transport.request('PUT', '/apimarketdata/instruments/subscription', token='market-data-token-1', body=subscription)
        print(f'Request answered {response.status}: {response.data.decode()}')
        print(f"First JSON object in a body without one: {self.transport.first_json_object('2:40')}")


if __name__ == '__main__':
    EngineIoHandshakeExample().run()

It prints:

HTTPS GET /apimarketdata/socket.io/: HTTP 200
Handshake 200: {'sid': 'engine-session-1', 'upgrades': ['websocket'], 'pingInterval': 25000, 'pingTimeout': 20000}
Heartbeat every 10.0 seconds
Heartbeat without limits from the server: 25.0 seconds
Websocket URL: wss://trade.wisdomcapital.in/apimarketdata/socket.io/?EIO=3&transport=websocket&sid=engine-session-1&token=market-data-token-1&userID=WCMD01&publishFormat=JSON&broadcastMode=Full
check_hostname=False verify_mode=True
HTTPS PUT /apimarketdata/instruments/subscription for message code 1501: HTTP 200
Request answered 200: {"type": "success"}
First JSON object in a body without one: None
examples/stock_brokers/websockets/wisdom_capital/WisdomCapitalTransport/example_2_pins_refusals_and_heartbeat.py
"""Checks certificate fingerprints, recognises refused tokens, and runs one heartbeat with `WisdomCapitalTransport`.

Once an XTS websocket opens, the sockets compare `peer_fingerprint`, the SHA-256 of the certificate the peer presented, with the pin; a connection whose certificate cannot be read counts as matching, since the HTTPS requests are pinned already. `is_authentication_refusal` recognises XTS's refused token answers by `e-session`, `e-token` or `Invalid Token`. `handshake` returns None for a refused handshake together with the status and body. `start_heartbeat` runs a thread that sends the Engine.IO ping `2` on an interval until the connection's `closed` event is set.

This program uses a stand-in connection presenting made-up certificate bytes and one presenting nothing, a stand-in `urllib3` package that refuses a handshake, and a heartbeat with a 0.05 second interval whose stand-in connection closes itself after the first ping. No network is used.

Notice that the made-up certificate does not match the pin, that `Instrument Already Subscribed` counts as a refusal to this check, which is why the quotes socket looks for it first, and that exactly one ping is sent.

Run it from the project root:

    python examples/stock_brokers/websockets/wisdom_capital/WisdomCapitalTransport/example_2_pins_refusals_and_heartbeat.py
"""

import json
import sys
import threading

from stock_brokers.websockets.wisdom_capital import (
    WisdomCapitalTransport,
)


class PinnedResponse:
    """A stand-in for the `urllib3` response of one XTS request.

    Attributes:
        status (int): The HTTP status.
        data (bytes): The body.
    """

    def __init__(self, status, text):
        """Keeps the answer.

        Args:
            status (int): The HTTP status.
            text (str): The body as text.

        Returns:
            None: This method returns nothing.
        """
        self.status = status
        self.data = text.encode()


class PinnedPool:
    """A stand-in for `urllib3.HTTPSConnectionPool` that answers from a shared script.

    Attributes:
        answers (list): The answers still to give, shared by every pool.
    """

    def __init__(self, answers):
        """Keeps the shared answers.

        Args:
            answers (list): The answers still to give, shared by every pool.

        Returns:
            None: This method returns nothing.
        """
        self.answers = answers

    def request(self, method, path, body=None, headers=None, timeout=None):
        """Prints the request and gives the next scripted answer.

        Args:
            method (str): The HTTP method.
            path (str): The path, with its query string.
            body (str | None): The JSON body.
            headers (dict | None): The request headers.
            timeout (int | None): The request timeout in seconds.

        Returns:
            PinnedResponse: The answer.
        """
        shown = path.split('?')[0]
        if body is not None:
            code = json.loads(body).get('xtsMessageCode')
            shown = f'{shown} for message code {code}'
        answer = self.answers.pop(0)
        print(f'HTTPS {method} {shown}: HTTP {answer.status}')
        return answer


class StandInUrllib3:
    """A stand-in for the `urllib3` package whose pools answer from one script.

    Attributes:
        answers (list): The answers, in the order requests are made.
    """

    def __init__(self, answers):
        """Keeps the answers.

        Args:
            answers (list): The answers, in the order requests are made.

        Returns:
            None: This method returns nothing.
        """
        self.answers = answers

    def HTTPSConnectionPool(self, host, port, assert_fingerprint=None):
        """Builds a pool, with the same signature as the real class.

        Args:
            host (str): The host.
            port (int): The port.
            assert_fingerprint (str | None): The certificate pin.

        Returns:
            PinnedPool: The pool.
        """
        return PinnedPool(self.answers)


class PeerCertificate:
    """A stand-in for the TLS socket inside a websocket, presenting fixed certificate bytes."""

    def getpeercert(self, binary_form=False):
        """The peer's certificate.

        Args:
            binary_form (bool): Whether to return the DER bytes.

        Returns:
            bytes: Made-up certificate bytes.
        """
        return b'not the pinned certificate'


class CertificateHolder:
    """A stand-in for the websocket-client socket object that holds the TLS socket.

    Attributes:
        sock (PeerCertificate): The TLS socket.
    """

    def __init__(self):
        """Holds the stand-in TLS socket.

        Returns:
            None: This method returns nothing.
        """
        self.sock = PeerCertificate()


class ConnectionWithCertificate:
    """A stand-in for an open websocket whose peer presented a certificate.

    Attributes:
        sock (CertificateHolder): The socket object.
    """

    def __init__(self):
        """Holds the stand-in socket object.

        Returns:
            None: This method returns nothing.
        """
        self.sock = CertificateHolder()


class ConnectionWithoutCertificate:
    """A stand-in for an open websocket whose certificate cannot be read."""


class PingedConnection:
    """A stand-in for an open websocket that records pings and closes after the first.

    Attributes:
        closed (threading.Event): The connection's closed event, set after the first ping.
        sent (list): The messages sent.
    """

    def __init__(self):
        """Starts open with nothing sent.

        Returns:
            None: This method returns nothing.
        """
        self.closed = threading.Event()
        self.sent = []

    def send(self, data):
        """Records a ping and closes the connection.

        Args:
            data (str): The message.

        Returns:
            None: This method returns nothing.
        """
        self.sent.append(data)
        self.closed.set()


class PinsRefusalsAndHeartbeatExample:
    """Checks fingerprints, refusals and one heartbeat.

    Attributes:
        transport (WisdomCapitalTransport): The transport being shown.
    """

    def __init__(self):
        """Registers the stand-in urllib3 package and builds the transport.

        Returns:
            None: This method returns nothing.
        """
        refused = {
            'type': 'error',
            'code': 'e-session-0001',
            'description': 'Invalid Token',
        }
        sys.modules['urllib3'] = StandInUrllib3([
            PinnedResponse(400, json.dumps(refused)),
        ])
        self.transport = WisdomCapitalTransport()

    def run(self):
        """Prints each check and the heartbeat's pings.

        Returns:
            None: This method returns nothing.
        """
        print(f'Made-up certificate: {self.transport.peer_fingerprint(ConnectionWithCertificate())[:16]}...')
        print(f'Unreadable certificate: {self.transport.peer_fingerprint(ConnectionWithoutCertificate())[:16]}...')
        answers = [
            '{"code": "e-session-0001", "description": "Invalid Token"}',
            '{"code": "e-session-0002", "description": "Instrument Already Subscribed !"}',
            '{"type": "success"}',
        ]
        for answer in answers:
            print(f'Refusal: {self.transport.is_authentication_refusal(answer)} for {answer}')
        handshake, status, body = self.transport.handshake('/interactive', 'token=stale&userID=WC0001&apiType=INTERACTIVE')
        print(f'Refused handshake: {handshake}, {status}, {body}')
        connection = PingedConnection()
        stop = threading.Event()
        self.transport.start_heartbeat(connection, connection.closed, 0.05, stop, 'example_heartbeat')
        connection.closed.wait(5)
        print(f'Heartbeat sent: {connection.sent}')


if __name__ == '__main__':
    PinsRefusalsAndHeartbeatExample().run()

It prints:

Made-up certificate: 2CC853CECC8C4B03...
Unreadable certificate: 1ACF7CA747F2D678...
Refusal: True for {"code": "e-session-0001", "description": "Invalid Token"}
Refusal: True for {"code": "e-session-0002", "description": "Instrument Already Subscribed !"}
Refusal: False for {"type": "success"}
HTTPS GET /interactive/socket.io/: HTTP 400
Refused handshake: None, 400, {"type": "error", "code": "e-session-0001", "description": "Invalid Token"}
Heartbeat sent: ['2']

Methods:

Name Description
request

Sends an HTTPS request to the platform over a connection pinned to its certificate.

handshake

Opens an Engine.IO session over HTTPS polling.

first_json_object

The first balanced {...} in an Engine.IO polling body, which frames the handshake JSON with a length prefix.

heartbeat_seconds

Half the shorter of the ping interval and timeout the server asked for, so a ping sits inside both.

websocket_url

The websocket URL that attaches to an Engine.IO session.

ssl_options

The TLS options for the websocket: the chain verified and the hostname check relaxed, since the pin is checked once it opens.

peer_fingerprint

The SHA-256 fingerprint of the certificate an open websocket's peer presented.

start_heartbeat

Starts a thread that sends the Engine.IO ping on the interval the server asked for, until the connection closes.

is_authentication_refusal

Whether an answer says the token was refused.

request

request(method, path, token=None, body=None)

Sends an HTTPS request to the platform over a connection pinned to its certificate.

Parameters:

Name Type Description Default
method str

The HTTP method.

required
path str

The path to request.

required
token str | None

The token to send as authorization, if any.

None
body object | None

An object to send as JSON, if any.

None

Returns:

Type Description
HTTPResponse

The response.

Raises:

Type Description
HTTPError

When the request fails or the certificate does not match the pin.

Example usage

From example_1_engine_io_handshake.py
response = self.transport.request('PUT', '/apimarketdata/instruments/subscription', token='market-data-token-1', body=subscription)
Source code in stock_brokers/websockets/wisdom_capital.py
def request(self, method, path, token=None, body=None):
    """
    Sends an HTTPS request to the platform over a connection pinned to its certificate.

    Args:
        method (str): The HTTP method.
        path (str): The path to request.
        token (str | None): The token to send as `authorization`, if any.
        body (object | None): An object to send as JSON, if any.

    Returns:
        urllib3.response.HTTPResponse: The response.

    Raises:
        urllib3.exceptions.HTTPError: When the request fails or the certificate does not match the pin.
    """
    import urllib3

    headers = {
        "Content-Type": "application/json",
    }
    if token:
        headers["authorization"] = token
    encoded_body = None
    if body is not None:
        encoded_body = json.dumps(body)
    pool = urllib3.HTTPSConnectionPool(HOST, PORT, assert_fingerprint=CERTIFICATE_FINGERPRINT)
    return pool.request(method, path, body=encoded_body, headers=headers, timeout=30)

handshake

handshake(path, query)

Opens an Engine.IO session over HTTPS polling.

Parameters:

Name Type Description Default
path str

The application path, market data or interactive.

required
query str

The query string that authenticates the session.

required

Returns:

Type Description
tuple

The decoded handshake, or None when it was refused or unreadable, and the answer's status and body for reporting.

Example usage

From example_1_engine_io_handshake.py
handshake, status, body = self.transport.handshake('/apimarketdata', query)
From example_2_pins_refusals_and_heartbeat.py
handshake, status, body = self.transport.handshake('/interactive', 'token=stale&userID=WC0001&apiType=INTERACTIVE')
Source code in stock_brokers/websockets/wisdom_capital.py
def handshake(self, path, query):
    """
    Opens an Engine.IO session over HTTPS polling.

    Args:
        path (str): The application path, market data or interactive.
        query (str): The query string that authenticates the session.

    Returns:
        tuple: The decoded handshake, or None when it was refused or unreadable, and the answer's status and body for reporting.
    """
    response = self.request("GET", f"{path}/socket.io/?EIO=3&transport=polling&{query}")
    body = response.data.decode("utf-8", errors="replace")
    handshake = None
    if response.status < 300:
        handshake = self.first_json_object(body)
    if not handshake or not handshake.get("sid"):
        handshake = None
    return handshake, response.status, body

first_json_object

first_json_object(text)

The first balanced {...} in an Engine.IO polling body, which frames the handshake JSON with a length prefix.

Parameters:

Name Type Description Default
text str

The body.

required

Returns:

Type Description
dict | None

The decoded object, or None when there is none.

Example usage

From example_1_engine_io_handshake.py
print(f"First JSON object in a body without one: {self.transport.first_json_object('2:40')}")
Source code in stock_brokers/websockets/wisdom_capital.py
def first_json_object(self, text):
    """
    The first balanced `{...}` in an Engine.IO polling body, which frames the handshake JSON with a length prefix.

    Args:
        text (str): The body.

    Returns:
        dict | None: The decoded object, or None when there is none.
    """
    start = text.find("{")
    if start == -1:
        return None
    depth = 0
    for index in range(start, len(text)):
        if text[index] == "{":
            depth = depth + 1
        elif text[index] == "}":
            depth = depth - 1
        if depth == 0:
            return json.loads(text[start:index + 1])
    return None

heartbeat_seconds

heartbeat_seconds(handshake, default_seconds)

Half the shorter of the ping interval and timeout the server asked for, so a ping sits inside both.

Parameters:

Name Type Description Default
handshake dict

The decoded Engine.IO handshake.

required
default_seconds float

The interval to use when the server asked for neither.

required

Returns:

Type Description
float

The interval in seconds, at least one.

Example usage

From example_1_engine_io_handshake.py
print(f'Heartbeat every {self.transport.heartbeat_seconds(handshake, 25.0)} seconds')
From example_1_engine_io_handshake.py
print(f'Heartbeat without limits from the server: {self.transport.heartbeat_seconds({}, 25.0)} seconds')
Source code in stock_brokers/websockets/wisdom_capital.py
def heartbeat_seconds(self, handshake, default_seconds):
    """
    Half the shorter of the ping interval and timeout the server asked for, so a ping sits inside both.

    Args:
        handshake (dict): The decoded Engine.IO handshake.
        default_seconds (float): The interval to use when the server asked for neither.

    Returns:
        float: The interval in seconds, at least one.
    """
    limits = []
    for value in (handshake.get("pingInterval"), handshake.get("pingTimeout")):
        if value:
            limits.append(float(value) / 1000.0)
    if not limits:
        return default_seconds
    return max(1.0, min(limits) / 2.0)

websocket_url

websocket_url(path, handshake, query)

The websocket URL that attaches to an Engine.IO session.

Parameters:

Name Type Description Default
path str

The application path, market data or interactive.

required
handshake dict

The decoded Engine.IO handshake.

required
query str

The query string that authenticates the session.

required

Returns:

Type Description
str

The URL.

Example usage

From example_1_engine_io_handshake.py
print(f"Websocket URL: {self.transport.websocket_url('/apimarketdata', handshake, query)}")
Source code in stock_brokers/websockets/wisdom_capital.py
def websocket_url(self, path, handshake, query):
    """
    The websocket URL that attaches to an Engine.IO session.

    Args:
        path (str): The application path, market data or interactive.
        handshake (dict): The decoded Engine.IO handshake.
        query (str): The query string that authenticates the session.

    Returns:
        str: The URL.
    """
    return f"wss://{HOST}{path}/socket.io/?EIO=3&transport=websocket&sid={handshake['sid']}&{query}"

ssl_options

ssl_options()

The TLS options for the websocket: the chain verified and the hostname check relaxed, since the pin is checked once it opens.

Returns:

Type Description
dict

The sslopt websocket-client takes.

Example usage

From example_1_engine_io_handshake.py
context = self.transport.ssl_options()['context']
Source code in stock_brokers/websockets/wisdom_capital.py
def ssl_options(self):
    """
    The TLS options for the websocket: the chain verified and the hostname check relaxed, since the pin is checked once it opens.

    Returns:
        dict: The `sslopt` websocket-client takes.
    """
    context = ssl.create_default_context()
    context.check_hostname = False
    return {
        "context": context,
    }

peer_fingerprint

peer_fingerprint(websocket_connection)

The SHA-256 fingerprint of the certificate an open websocket's peer presented.

Parameters:

Name Type Description Default
websocket_connection WebSocketApp

The open connection.

required

Returns:

Type Description
str

The fingerprint in upper case hex, or the pinned one when the certificate cannot be read.

Example usage

From example_2_pins_refusals_and_heartbeat.py
print(f'Made-up certificate: {self.transport.peer_fingerprint(ConnectionWithCertificate())[:16]}...')
From example_2_pins_refusals_and_heartbeat.py
print(f'Unreadable certificate: {self.transport.peer_fingerprint(ConnectionWithoutCertificate())[:16]}...')
Source code in stock_brokers/websockets/wisdom_capital.py
def peer_fingerprint(self, websocket_connection):
    """
    The SHA-256 fingerprint of the certificate an open websocket's peer presented.

    Args:
        websocket_connection (websocket.WebSocketApp): The open connection.

    Returns:
        str: The fingerprint in upper case hex, or the pinned one when the certificate cannot be read.
    """
    try:
        return hashlib.sha256(websocket_connection.sock.sock.getpeercert(True)).hexdigest().upper()
    except (AttributeError, TypeError):
        return CERTIFICATE_FINGERPRINT

start_heartbeat

start_heartbeat(websocket_connection, closed, interval, stop, thread_name)

Starts a thread that sends the Engine.IO ping on the interval the server asked for, until the connection closes.

Parameters:

Name Type Description Default
websocket_connection WebSocketApp

The connection to ping.

required
closed Event

Set when this connection closes.

required
interval float

The interval in seconds.

required
stop Event

Set when the socket is stopped.

required
thread_name str

The thread's name.

required

Returns:

Type Description
None

This method returns nothing.

Example usage

From example_2_pins_refusals_and_heartbeat.py
self.transport.start_heartbeat(connection, connection.closed, 0.05, stop, 'example_heartbeat')
Source code in stock_brokers/websockets/wisdom_capital.py
def start_heartbeat(self, websocket_connection, closed, interval, stop, thread_name):
    """
    Starts a thread that sends the Engine.IO ping on the interval the server asked for, until the connection closes.

    Args:
        websocket_connection (websocket.WebSocketApp): The connection to ping.
        closed (threading.Event): Set when this connection closes.
        interval (float): The interval in seconds.
        stop (threading.Event): Set when the socket is stopped.
        thread_name (str): The thread's name.

    Returns:
        None: This method returns nothing.
    """
    thread = threading.Thread(
        target=self._send_heartbeats,
        args=(websocket_connection, closed, interval, stop),
        name=thread_name,
        daemon=True,
    )
    thread.start()

_send_heartbeats

_send_heartbeats(websocket_connection, closed, interval, stop)

Sends 2 every interval until the connection closes, the socket stops, or a send fails.

Parameters:

Name Type Description Default
websocket_connection WebSocketApp

The connection to ping.

required
closed Event

Set when this connection closes.

required
interval float

The interval in seconds.

required
stop Event

Set when the socket is stopped.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/wisdom_capital.py
def _send_heartbeats(self, websocket_connection, closed, interval, stop):
    """
    Sends `2` every interval until the connection closes, the socket stops, or a send fails.

    Args:
        websocket_connection (websocket.WebSocketApp): The connection to ping.
        closed (threading.Event): Set when this connection closes.
        interval (float): The interval in seconds.
        stop (threading.Event): Set when the socket is stopped.

    Returns:
        None: This method returns nothing.
    """
    while not closed.wait(interval) and not stop.is_set():
        try:
            websocket_connection.send("2")
        except Exception:
            return

is_authentication_refusal

is_authentication_refusal(text)

Whether an answer says the token was refused.

Parameters:

Name Type Description Default
text str

The answer's body.

required

Returns:

Type Description
bool

True for a refused token.

Example usage

From example_2_pins_refusals_and_heartbeat.py
print(f'Refusal: {self.transport.is_authentication_refusal(answer)} for {answer}')
Source code in stock_brokers/websockets/wisdom_capital.py
def is_authentication_refusal(self, text):
    """
    Whether an answer says the token was refused.

    Args:
        text (str): The answer's body.

    Returns:
        bool: True for a refused token.
    """
    lowered = str(text).lower()
    for marker in AUTHENTICATION_MARKERS:
        if marker in lowered:
            return True
    return False

WisdomCapitalMarketDataSession

WisdomCapitalMarketDataSession(logger)

The market data login every quotes socket authenticates with.

Constructs WisdomCapitalAPI, which establishes both of Wisdom Capital's sessions when either has stopped working.

Parameters:

Name Type Description Default
logger Logger

Where token replacements are reported.

required

Returns:

Type Description
None

This method returns nothing.

Raises:

Type Description
Exception

Whatever WisdomCapitalAPI raises, or the market data login failure it recorded.

Example programs

examples/stock_brokers/websockets/wisdom_capital/WisdomCapitalMarketDataSession/example_1_reading_and_replacing_the_token.py
"""Reads Wisdom Capital's market data token and replaces it, once, when a socket reports it refused.

XTS issues one market data token per application key, and a second login invalidates the first, so `WisdomCapitalMarketDataSession` never logs in on its own. `market_data` reads the token and user id in force from `WisdomCapitalAPI`, and `replace` asks the API to replace a refused token, which the API does under a Redis lock and only when nobody has replaced it already.

Constructing the real `WisdomCapitalAPI` logs in to both XTS applications, so the program registers a stand-in module under `stock_brokers.api.wisdom_capital` that issues numbered market data tokens and replaces one only when it is still the stale one.

Notice that the second `replace` with the same stale token makes no login.

Run it from the project root:

    python examples/stock_brokers/websockets/wisdom_capital/WisdomCapitalMarketDataSession/example_1_reading_and_replacing_the_token.py
"""

import base64
import json
import logging
import sys
import types

from stock_brokers.websockets.wisdom_capital import (
    WisdomCapitalMarketDataSession,
)


class LoginRecord:
    """The Wisdom Capital login state every stand-in API object reads, standing in for the Redis `last_login` hash.

    Attributes:
        market_data_token (str | None): The market data token in force now.
        interactive_user_id (str | None): The user id inside the interactive token in force now.
        interactive_session (str | None): The session inside the interactive token in force now.
        login_count (int): How many interactive logins the stand-in API has made.
        market_data_logins (int): How many market data logins the stand-in API has made.
        market_data_fails (bool): Whether the market data login fails.
    """

    market_data_token = None
    interactive_user_id = 'WC0001'
    interactive_session = None
    login_count = 0
    market_data_logins = 0
    market_data_fails = False


class StandInWisdomCapitalAPI:
    """A stand-in for `WisdomCapitalAPI` whose construction is a pretend login to both XTS applications.

    Attributes:
        market_data_session_error (Exception | None): The market data login failure, recorded rather than raised, as the real class does.
    """

    def __init__(self):
        """Logs in, which here only issues numbered tokens.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.login_count = LoginRecord.login_count + 1
        LoginRecord.interactive_session = f'interactive-{LoginRecord.login_count}'
        print(f'Stand-in login number {LoginRecord.login_count}')
        self.market_data_session_error = None
        if LoginRecord.market_data_fails:
            self.market_data_session_error = RuntimeError('the market data login was refused: Invalid secret key')
        elif LoginRecord.market_data_token is None:
            self.log_in_to_market_data()

    def log_in_to_market_data(self):
        """Issues the next numbered market data token.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.market_data_logins = LoginRecord.market_data_logins + 1
        LoginRecord.market_data_token = f'market-data-token-{LoginRecord.market_data_logins}'

    def _current_login(self):
        """The interactive login in force now, its token a JWT carrying the user id as XTS signs it.

        Returns:
            dict: The access token.
        """
        if LoginRecord.interactive_session is None:
            return {
                'access_token': None,
            }
        claims = {
            'session': LoginRecord.interactive_session,
        }
        if LoginRecord.interactive_user_id is not None:
            claims['userID'] = LoginRecord.interactive_user_id
        payload = base64.urlsafe_b64encode(json.dumps(claims).encode()).decode().rstrip('=')
        return {
            'access_token': f'header.{payload}.signature',
        }

    def market_data_session(self):
        """The market data token and user id in force now.

        Returns:
            dict: `access_token` and `user_id`.
        """
        return {
            'access_token': LoginRecord.market_data_token,
            'user_id': 'WCMD01',
        }

    def replace_market_data_session(self, stale_access_token=None):
        """Logs in to market data again, unless the token in force is no longer the stale one.

        Args:
            stale_access_token (str | None): The token that was refused.

        Returns:
            dict: The market data session now in force.
        """
        if LoginRecord.market_data_token == stale_access_token:
            self.log_in_to_market_data()
            print(f'Stand-in market data login issued {LoginRecord.market_data_token}')
        else:
            print('The market data token was already replaced; no login.')
        return self.market_data_session()


class ReadingAndReplacingTheTokenExample:
    """Reads and replaces a market data token.

    Attributes:
        session (WisdomCapitalMarketDataSession): The session being shown.
    """

    def __init__(self):
        """Registers the stand-in API module and builds the session.

        Returns:
            None: This method returns nothing.
        """
        logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
        api_module = types.ModuleType('stock_brokers.api.wisdom_capital')
        api_module.WisdomCapitalAPI = StandInWisdomCapitalAPI
        sys.modules['stock_brokers.api.wisdom_capital'] = api_module
        self.session = WisdomCapitalMarketDataSession(logging.getLogger('wisdom_capital.session'))

    def run(self):
        """Prints the session, replaces its token twice with the same stale token, and prints it again.

        Returns:
            None: This method returns nothing.
        """
        market_data = self.session.market_data()
        print(f'Market data session: {market_data}')
        stale_token = market_data['access_token']
        self.session.replace(stale_token)
        self.session.replace(stale_token)
        print(f'Market data session: {self.session.market_data()}')


if __name__ == '__main__':
    ReadingAndReplacingTheTokenExample().run()

It prints:

Stand-in login number 1
Market data session: {'access_token': 'market-data-token-1', 'user_id': 'WCMD01'}
WARNING Replacing the Wisdom Capital market data token.
Stand-in market data login issued market-data-token-2
WARNING Replacing the Wisdom Capital market data token.
The market data token was already replaced; no login.
Market data session: {'access_token': 'market-data-token-2', 'user_id': 'WCMD01'}
examples/stock_brokers/websockets/wisdom_capital/WisdomCapitalMarketDataSession/example_2_market_data_login_failed.py
"""Shows `WisdomCapitalMarketDataSession` raising the market data login failure the API recorded.

`WisdomCapitalAPI` establishes both XTS sessions when it is constructed, and records a failed market data login in `market_data_session_error` rather than raising it, because the interactive session may still work. `WisdomCapitalMarketDataSession` needs the market data session, so its constructor raises that recorded error, and the quotes script exits instead of connecting without a token.

Constructing the real `WisdomCapitalAPI` logs in to both XTS applications, so the program registers a stand-in module under `stock_brokers.api.wisdom_capital` whose market data login is scripted to fail.

Notice that the stand-in's login itself succeeds, and that the error comes from building the session.

Run it from the project root:

    python examples/stock_brokers/websockets/wisdom_capital/WisdomCapitalMarketDataSession/example_2_market_data_login_failed.py
"""

import base64
import json
import logging
import sys
import types

from stock_brokers.websockets.wisdom_capital import (
    WisdomCapitalMarketDataSession,
)


class LoginRecord:
    """The Wisdom Capital login state every stand-in API object reads, standing in for the Redis `last_login` hash.

    Attributes:
        market_data_token (str | None): The market data token in force now.
        interactive_user_id (str | None): The user id inside the interactive token in force now.
        interactive_session (str | None): The session inside the interactive token in force now.
        login_count (int): How many interactive logins the stand-in API has made.
        market_data_logins (int): How many market data logins the stand-in API has made.
        market_data_fails (bool): Whether the market data login fails.
    """

    market_data_token = None
    interactive_user_id = 'WC0001'
    interactive_session = None
    login_count = 0
    market_data_logins = 0
    market_data_fails = False


class StandInWisdomCapitalAPI:
    """A stand-in for `WisdomCapitalAPI` whose construction is a pretend login to both XTS applications.

    Attributes:
        market_data_session_error (Exception | None): The market data login failure, recorded rather than raised, as the real class does.
    """

    def __init__(self):
        """Logs in, which here only issues numbered tokens.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.login_count = LoginRecord.login_count + 1
        LoginRecord.interactive_session = f'interactive-{LoginRecord.login_count}'
        print(f'Stand-in login number {LoginRecord.login_count}')
        self.market_data_session_error = None
        if LoginRecord.market_data_fails:
            self.market_data_session_error = RuntimeError('the market data login was refused: Invalid secret key')
        elif LoginRecord.market_data_token is None:
            self.log_in_to_market_data()

    def log_in_to_market_data(self):
        """Issues the next numbered market data token.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.market_data_logins = LoginRecord.market_data_logins + 1
        LoginRecord.market_data_token = f'market-data-token-{LoginRecord.market_data_logins}'

    def _current_login(self):
        """The interactive login in force now, its token a JWT carrying the user id as XTS signs it.

        Returns:
            dict: The access token.
        """
        if LoginRecord.interactive_session is None:
            return {
                'access_token': None,
            }
        claims = {
            'session': LoginRecord.interactive_session,
        }
        if LoginRecord.interactive_user_id is not None:
            claims['userID'] = LoginRecord.interactive_user_id
        payload = base64.urlsafe_b64encode(json.dumps(claims).encode()).decode().rstrip('=')
        return {
            'access_token': f'header.{payload}.signature',
        }

    def market_data_session(self):
        """The market data token and user id in force now.

        Returns:
            dict: `access_token` and `user_id`.
        """
        return {
            'access_token': LoginRecord.market_data_token,
            'user_id': 'WCMD01',
        }

    def replace_market_data_session(self, stale_access_token=None):
        """Logs in to market data again, unless the token in force is no longer the stale one.

        Args:
            stale_access_token (str | None): The token that was refused.

        Returns:
            dict: The market data session now in force.
        """
        if LoginRecord.market_data_token == stale_access_token:
            self.log_in_to_market_data()
            print(f'Stand-in market data login issued {LoginRecord.market_data_token}')
        else:
            print('The market data token was already replaced; no login.')
        return self.market_data_session()


class MarketDataLoginFailedExample:
    """Builds a market data session whose login failed.

    Attributes:
        logger (logging.Logger): Where the session would report.
    """

    def __init__(self):
        """Registers the stand-in API module and scripts the market data login to fail.

        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.wisdom_capital')
        api_module.WisdomCapitalAPI = StandInWisdomCapitalAPI
        sys.modules['stock_brokers.api.wisdom_capital'] = api_module
        LoginRecord.market_data_fails = True
        self.logger = logging.getLogger('wisdom_capital.session')

    def run(self):
        """Builds the session and prints the error, or the session when it builds.

        Returns:
            None: This method returns nothing.
        """
        try:
            session = WisdomCapitalMarketDataSession(self.logger)
            print(f'Market data session: {session.market_data()}')
        except RuntimeError as error:
            print(f'The market data session could not start: {error}')


if __name__ == '__main__':
    MarketDataLoginFailedExample().run()

It prints:

Stand-in login number 1
The market data session could not start: the market data login was refused: Invalid secret key

Methods:

Name Description
market_data

The market data session in force now.

replace

Replaces a refused market data token, unless another socket or process already has.

Source code in stock_brokers/websockets/wisdom_capital.py
def __init__(self, logger):
    """
    Constructs `WisdomCapitalAPI`, which establishes both of Wisdom Capital's sessions when either has stopped working.

    Args:
        logger (logging.Logger): Where token replacements are reported.

    Returns:
        None: This method returns nothing.

    Raises:
        Exception: Whatever `WisdomCapitalAPI` raises, or the market data login failure it recorded.
    """
    from stock_brokers.api.wisdom_capital import WisdomCapitalAPI

    self._api = WisdomCapitalAPI()
    self._logger = logger
    self._lock = threading.Lock()
    if self._api.market_data_session_error:
        raise self._api.market_data_session_error

market_data

market_data()

The market data session in force now.

Returns:

Type Description
dict

access_token and user_id.

Example usage

From example_1_reading_and_replacing_the_token.py
market_data = self.session.market_data()
From example_1_reading_and_replacing_the_token.py
print(f'Market data session: {self.session.market_data()}')
From example_2_market_data_login_failed.py
print(f'Market data session: {session.market_data()}')
Source code in stock_brokers/websockets/wisdom_capital.py
def market_data(self):
    """
    The market data session in force now.

    Returns:
        dict: `access_token` and `user_id`.
    """
    return self._api.market_data_session()

replace

replace(stale_token)

Replaces a refused market data token, unless another socket or process already has.

Parameters:

Name Type Description Default
stale_token str | None

The token the failing connection used.

required

Returns:

Type Description
None

This method returns nothing.

Raises:

Type Description
Exception

Whatever WisdomCapitalAPI raises when it cannot log in.

Example usage

From example_1_reading_and_replacing_the_token.py
self.session.replace(stale_token)
Source code in stock_brokers/websockets/wisdom_capital.py
def replace(self, stale_token):
    """
    Replaces a refused market data token, unless another socket or process already has.

    Args:
        stale_token (str | None): The token the failing connection used.

    Returns:
        None: This method returns nothing.

    Raises:
        Exception: Whatever `WisdomCapitalAPI` raises when it cannot log in.
    """
    with self._lock:
        self._logger.warning("Replacing the Wisdom Capital market data token.")
        self._api.replace_market_data_session(stale_access_token=stale_token)

WisdomCapitalInteractiveSession

WisdomCapitalInteractiveSession(logger)

The interactive login the order updates socket joins with, shared with every Wisdom Capital process.

Constructs WisdomCapitalAPI, 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 WisdomCapitalAPI raises when it cannot log in.

Example programs

examples/stock_brokers/websockets/wisdom_capital/WisdomCapitalInteractiveSession/example_1_shared_login_and_user_id.py
"""Reads the interactive token Wisdom Capital processes share and the user id inside it, then checks the login again.

`WisdomCapitalInteractiveSession` joins with the interactive token every Wisdom Capital process shares through `last_login`. `current_token` returns that token, `shared_login` returns it together with the `userID` claim of its JWT payload, which XTS wants in the connection query, and `log_in_again_without_checking` constructs `WisdomCapitalAPI` again, which keeps a stored token that still works and logs in only when it does not.

Constructing the real `WisdomCapitalAPI` logs in to both XTS applications, so the program registers a stand-in module under `stock_brokers.api.wisdom_capital` whose tokens are JWTs carrying the user id. Each construction counts as one login.

Notice that the user id is read from inside the token, and that the token changes after the login check.

Run it from the project root:

    python examples/stock_brokers/websockets/wisdom_capital/WisdomCapitalInteractiveSession/example_1_shared_login_and_user_id.py
"""

import base64
import json
import logging
import sys
import types

from stock_brokers.websockets.wisdom_capital import (
    WisdomCapitalInteractiveSession,
)


class LoginRecord:
    """The Wisdom Capital login state every stand-in API object reads, standing in for the Redis `last_login` hash.

    Attributes:
        market_data_token (str | None): The market data token in force now.
        interactive_user_id (str | None): The user id inside the interactive token in force now.
        interactive_session (str | None): The session inside the interactive token in force now.
        login_count (int): How many interactive logins the stand-in API has made.
        market_data_logins (int): How many market data logins the stand-in API has made.
        market_data_fails (bool): Whether the market data login fails.
    """

    market_data_token = None
    interactive_user_id = 'WC0001'
    interactive_session = None
    login_count = 0
    market_data_logins = 0
    market_data_fails = False


class StandInWisdomCapitalAPI:
    """A stand-in for `WisdomCapitalAPI` whose construction is a pretend login to both XTS applications.

    Attributes:
        market_data_session_error (Exception | None): The market data login failure, recorded rather than raised, as the real class does.
    """

    def __init__(self):
        """Logs in, which here only issues numbered tokens.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.login_count = LoginRecord.login_count + 1
        LoginRecord.interactive_session = f'interactive-{LoginRecord.login_count}'
        print(f'Stand-in login number {LoginRecord.login_count}')
        self.market_data_session_error = None
        if LoginRecord.market_data_fails:
            self.market_data_session_error = RuntimeError('the market data login was refused: Invalid secret key')
        elif LoginRecord.market_data_token is None:
            self.log_in_to_market_data()

    def log_in_to_market_data(self):
        """Issues the next numbered market data token.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.market_data_logins = LoginRecord.market_data_logins + 1
        LoginRecord.market_data_token = f'market-data-token-{LoginRecord.market_data_logins}'

    def _current_login(self):
        """The interactive login in force now, its token a JWT carrying the user id as XTS signs it.

        Returns:
            dict: The access token.
        """
        if LoginRecord.interactive_session is None:
            return {
                'access_token': None,
            }
        claims = {
            'session': LoginRecord.interactive_session,
        }
        if LoginRecord.interactive_user_id is not None:
            claims['userID'] = LoginRecord.interactive_user_id
        payload = base64.urlsafe_b64encode(json.dumps(claims).encode()).decode().rstrip('=')
        return {
            'access_token': f'header.{payload}.signature',
        }

    def market_data_session(self):
        """The market data token and user id in force now.

        Returns:
            dict: `access_token` and `user_id`.
        """
        return {
            'access_token': LoginRecord.market_data_token,
            'user_id': 'WCMD01',
        }

    def replace_market_data_session(self, stale_access_token=None):
        """Logs in to market data again, unless the token in force is no longer the stale one.

        Args:
            stale_access_token (str | None): The token that was refused.

        Returns:
            dict: The market data session now in force.
        """
        if LoginRecord.market_data_token == stale_access_token:
            self.log_in_to_market_data()
            print(f'Stand-in market data login issued {LoginRecord.market_data_token}')
        else:
            print('The market data token was already replaced; no login.')
        return self.market_data_session()


class SharedLoginAndUserIdExample:
    """Reads the shared interactive login around a login check.

    Attributes:
        session (WisdomCapitalInteractiveSession): The session being shown.
    """

    def __init__(self):
        """Registers the stand-in API module and builds the session.

        Returns:
            None: This method returns nothing.
        """
        logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
        api_module = types.ModuleType('stock_brokers.api.wisdom_capital')
        api_module.WisdomCapitalAPI = StandInWisdomCapitalAPI
        sys.modules['stock_brokers.api.wisdom_capital'] = api_module
        self.session = WisdomCapitalInteractiveSession(logging.getLogger('wisdom_capital.session'))

    def claims(self, token):
        """The JWT claims of a token, decoded for display.

        Args:
            token (str): The token.

        Returns:
            dict: The claims.
        """
        payload = token.split('.')[1]
        return json.loads(base64.urlsafe_b64decode(payload + '=' * (-len(payload) % 4)))

    def run(self):
        """Prints the shared login before and after a login check.

        Returns:
            None: This method returns nothing.
        """
        token, user_id = self.session.shared_login()
        print(f'User id {user_id}, token claims {self.claims(token)}')
        self.session.log_in_again_without_checking()
        print(f'Token claims after the check: {self.claims(self.session.current_token())}')


if __name__ == '__main__':
    SharedLoginAndUserIdExample().run()

It prints:

Stand-in login number 1
User id WC0001, token claims {'session': 'interactive-1', 'userID': 'WC0001'}
WARNING Checking the Wisdom Capital login, and logging in again if it is dead.
Stand-in login number 2
Token claims after the check: {'session': 'interactive-2', 'userID': 'WC0001'}
examples/stock_brokers/websockets/wisdom_capital/WisdomCapitalInteractiveSession/example_2_unusable_stored_tokens.py
"""Shows `WisdomCapitalInteractiveSession.shared_login` refusing a missing token and a token without a user id.

`shared_login` raises `RuntimeError` when no usable interactive token is stored, or when the stored token's JWT payload carries no `userID`, because the socket cannot join the interactive namespace without both. The order update socket takes either as a refused login.

Constructing the real `WisdomCapitalAPI` logs in to both XTS applications, so the program registers a stand-in module under `stock_brokers.api.wisdom_capital`. The program then empties the shared login, and afterwards stores a token without the claim, as a broken login would.

Notice that `current_token` answers None rather than raising, and that the two errors say which part is missing.

Run it from the project root:

    python examples/stock_brokers/websockets/wisdom_capital/WisdomCapitalInteractiveSession/example_2_unusable_stored_tokens.py
"""

import base64
import json
import logging
import sys
import types

from stock_brokers.websockets.wisdom_capital import (
    WisdomCapitalInteractiveSession,
)


class LoginRecord:
    """The Wisdom Capital login state every stand-in API object reads, standing in for the Redis `last_login` hash.

    Attributes:
        market_data_token (str | None): The market data token in force now.
        interactive_user_id (str | None): The user id inside the interactive token in force now.
        interactive_session (str | None): The session inside the interactive token in force now.
        login_count (int): How many interactive logins the stand-in API has made.
        market_data_logins (int): How many market data logins the stand-in API has made.
        market_data_fails (bool): Whether the market data login fails.
    """

    market_data_token = None
    interactive_user_id = 'WC0001'
    interactive_session = None
    login_count = 0
    market_data_logins = 0
    market_data_fails = False


class StandInWisdomCapitalAPI:
    """A stand-in for `WisdomCapitalAPI` whose construction is a pretend login to both XTS applications.

    Attributes:
        market_data_session_error (Exception | None): The market data login failure, recorded rather than raised, as the real class does.
    """

    def __init__(self):
        """Logs in, which here only issues numbered tokens.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.login_count = LoginRecord.login_count + 1
        LoginRecord.interactive_session = f'interactive-{LoginRecord.login_count}'
        print(f'Stand-in login number {LoginRecord.login_count}')
        self.market_data_session_error = None
        if LoginRecord.market_data_fails:
            self.market_data_session_error = RuntimeError('the market data login was refused: Invalid secret key')
        elif LoginRecord.market_data_token is None:
            self.log_in_to_market_data()

    def log_in_to_market_data(self):
        """Issues the next numbered market data token.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.market_data_logins = LoginRecord.market_data_logins + 1
        LoginRecord.market_data_token = f'market-data-token-{LoginRecord.market_data_logins}'

    def _current_login(self):
        """The interactive login in force now, its token a JWT carrying the user id as XTS signs it.

        Returns:
            dict: The access token.
        """
        if LoginRecord.interactive_session is None:
            return {
                'access_token': None,
            }
        claims = {
            'session': LoginRecord.interactive_session,
        }
        if LoginRecord.interactive_user_id is not None:
            claims['userID'] = LoginRecord.interactive_user_id
        payload = base64.urlsafe_b64encode(json.dumps(claims).encode()).decode().rstrip('=')
        return {
            'access_token': f'header.{payload}.signature',
        }

    def market_data_session(self):
        """The market data token and user id in force now.

        Returns:
            dict: `access_token` and `user_id`.
        """
        return {
            'access_token': LoginRecord.market_data_token,
            'user_id': 'WCMD01',
        }

    def replace_market_data_session(self, stale_access_token=None):
        """Logs in to market data again, unless the token in force is no longer the stale one.

        Args:
            stale_access_token (str | None): The token that was refused.

        Returns:
            dict: The market data session now in force.
        """
        if LoginRecord.market_data_token == stale_access_token:
            self.log_in_to_market_data()
            print(f'Stand-in market data login issued {LoginRecord.market_data_token}')
        else:
            print('The market data token was already replaced; no login.')
        return self.market_data_session()


class UnusableStoredTokensExample:
    """Asks for the shared login when the stored token is unusable.

    Attributes:
        session (WisdomCapitalInteractiveSession): The session being shown.
    """

    def __init__(self):
        """Registers the stand-in API module and builds the session.

        Returns:
            None: This method returns nothing.
        """
        logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
        api_module = types.ModuleType('stock_brokers.api.wisdom_capital')
        api_module.WisdomCapitalAPI = StandInWisdomCapitalAPI
        sys.modules['stock_brokers.api.wisdom_capital'] = api_module
        self.session = WisdomCapitalInteractiveSession(logging.getLogger('wisdom_capital.session'))

    def try_shared_login(self):
        """Asks for the shared login and prints the outcome.

        Returns:
            None: This method returns nothing.
        """
        try:
            token, user_id = self.session.shared_login()
            print(f'Shared login for {user_id}')
        except RuntimeError as error:
            print(f'RuntimeError: {error}')

    def run(self):
        """Empties the stored login, then stores one without a user id, asking for the shared login each time.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.interactive_session = None
        print(f'Current token: {self.session.current_token()}')
        self.try_shared_login()
        LoginRecord.interactive_session = 'interactive-without-user'
        LoginRecord.interactive_user_id = None
        self.try_shared_login()


if __name__ == '__main__':
    UnusableStoredTokensExample().run()

It prints:

Stand-in login number 1
Current token: None
RuntimeError: no Wisdom Capital access token is stored in last_login
RuntimeError: the stored Wisdom Capital access token carries no userID

Methods:

Name Description
current_token

The interactive token stored in the shared login now.

shared_login

The interactive token every Wisdom Capital process shares, and the user id it was issued to.

log_in_again_without_checking

Constructs WisdomCapitalAPI again, which keeps a stored token that still works and logs in only when it does not.

Source code in stock_brokers/websockets/wisdom_capital.py
def __init__(self, logger):
    """
    Constructs `WisdomCapitalAPI`, which logs in when the stored session is dead.

    Args:
        logger (logging.Logger): Where logins are reported.

    Returns:
        None: This method returns nothing.

    Raises:
        Exception: Whatever `WisdomCapitalAPI` raises when it cannot log in.
    """
    from stock_brokers.api.wisdom_capital import WisdomCapitalAPI

    self._api_class = WisdomCapitalAPI
    self._api = WisdomCapitalAPI()
    self._logger = logger

current_token

current_token()

The interactive token stored in the shared login now.

Returns:

Type Description
str | None

The token, or None when none is stored.

Example usage

From example_1_shared_login_and_user_id.py
print(f'Token claims after the check: {self.claims(self.session.current_token())}')
From example_2_unusable_stored_tokens.py
print(f'Current token: {self.session.current_token()}')
Source code in stock_brokers/websockets/wisdom_capital.py
def current_token(self):
    """
    The interactive token stored in the shared login now.

    Returns:
        str | None: The token, or None when none is stored.
    """
    login = self._api._current_login() or {}
    return login.get("access_token")

shared_login

shared_login()

The interactive token every Wisdom Capital process shares, and the user id it was issued to.

Returns:

Type Description
tuple

The access token and the user id.

Raises:

Type Description
RuntimeError

No usable token is stored, or the stored token carries no user id.

Example usage

From example_1_shared_login_and_user_id.py
token, user_id = self.session.shared_login()
From example_2_unusable_stored_tokens.py
token, user_id = self.session.shared_login()
Source code in stock_brokers/websockets/wisdom_capital.py
def shared_login(self):
    """
    The interactive token every Wisdom Capital process shares, and the user id it was issued to.

    Returns:
        tuple: The access token and the user id.

    Raises:
        RuntimeError: No usable token is stored, or the stored token carries no user id.
    """
    token = self.current_token()
    if not token or token == "None":
        raise RuntimeError("no Wisdom Capital access token is stored in last_login")
    user_id = self._user_id_from_token(token)
    if user_id is None:
        raise RuntimeError("the stored Wisdom Capital access token carries no userID")
    return token, user_id

log_in_again_without_checking

log_in_again_without_checking()

Constructs WisdomCapitalAPI again, which keeps a stored token that still works and logs in only when it does not.

Returns:

Type Description
None

This method returns nothing.

Raises:

Type Description
Exception

Whatever WisdomCapitalAPI raises when it cannot log in.

Example usage

From example_1_shared_login_and_user_id.py
self.session.log_in_again_without_checking()
Source code in stock_brokers/websockets/wisdom_capital.py
def log_in_again_without_checking(self):
    """
    Constructs `WisdomCapitalAPI` again, which keeps a stored token that still works and logs in only when it does not.

    Returns:
        None: This method returns nothing.

    Raises:
        Exception: Whatever `WisdomCapitalAPI` raises when it cannot log in.
    """
    self._logger.warning("Checking the Wisdom Capital login, and logging in again if it is dead.")
    self._api = self._api_class()

_user_id_from_token

_user_id_from_token(token)

The user id XTS signs into an interactive token.

Parameters:

Name Type Description Default
token str

The interactive access token, a JWT.

required

Returns:

Type Description
str | None

The token's userID claim, or None when the token is not a readable JWT or has no such claim.

Source code in stock_brokers/websockets/wisdom_capital.py
def _user_id_from_token(self, token):
    """
    The user id XTS signs into an interactive token.

    Args:
        token (str): The interactive access token, a JWT.

    Returns:
        str | None: The token's `userID` claim, or None when the token is not a readable JWT or has no such claim.
    """
    parts = token.split(".")
    if len(parts) != 3:
        return None
    padded_payload = parts[1] + "=" * (-len(parts[1]) % 4)
    try:
        payload = json.loads(base64.urlsafe_b64decode(padded_payload))
    except ValueError:
        return None
    if not isinstance(payload, dict):
        return None
    return payload.get("userID")

WisdomCapitalQuotesSocket

WisdomCapitalQuotesSocket(name, tokens, names, session, on_ticks, logger)

Bases: BrokerWebsocket

One XTS market data socket carrying the touchline and depth of a batch of instruments.

Sets up a socket for one batch of instruments.

Parameters:

Name Type Description Default
name str

The connection's name, for the log.

required
tokens list[str]

The batch of SEGMENT:EXCHANGEINSTRUMENTID instrument tokens.

required
names dict[str, str]

Each token to the instrument's name, which becomes a tick's id.

required
session WisdomCapitalMarketDataSession

The shared market data login.

required
on_ticks callable

Called with each 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

examples/stock_brokers/websockets/wisdom_capital/WisdomCapitalQuotesSocket/example_1_snapshot_then_depth.py
"""Runs a Wisdom Capital XTS quotes socket through the Engine.IO handshake, the REST subscription snapshot and a depth event.

`WisdomCapitalQuotesSocket` opens an Engine.IO session over pinned HTTPS polling with the market data token, attaches a websocket to it, and completes the upgrade: it sends `2probe`, and when the server answers `3probe` it sends `5`, starts pinging, and subscribes its instruments over REST for touchline (1501) and market depth (1502). Each subscription answer carries the current snapshot, handed on at once. Events then arrive as socket.io frames `42["<event>", <payload>]`; touchline and depth for one instrument are merged, and XTS timestamps counted from 1980 in India time are moved onto the Unix epoch.

This program subscribes NSE RELIANCE (`1:2885`). The touchline subscription's snapshot carries a touchline and the depth subscription's carries nothing, then the websocket delivers a server ping and a depth event nesting a new touchline. No real socket or HTTPS request is made: a stand-in `urllib3` package answers the handshake and the subscriptions, a stand-in `websocket` package plays the frames and prints what the socket sends, and a stand-in `stock_brokers.api.wisdom_capital` module lets the real `WisdomCapitalMarketDataSession` supply the market data token. The stand-in connection presents no certificate, so the pin check falls back to the pinned fingerprint.

Notice the `2probe`, `5` and `3` the socket sends, that the snapshot's tick has a one-level book from the touchline's best bid and offer, that the depth event's tick has five levels because the padded zero row is dropped, and that the misspelt `LastTradedQunatity` still fills `last_quantity`.

Run it from the project root:

    python examples/stock_brokers/websockets/wisdom_capital/WisdomCapitalQuotesSocket/example_1_snapshot_then_depth.py
"""

import base64
import json
import logging
import sys
import types

from stock_brokers.websockets.wisdom_capital import (
    WisdomCapitalMarketDataSession,
    WisdomCapitalQuotesSocket,
)


class LoginRecord:
    """The Wisdom Capital login state every stand-in API object reads, standing in for the Redis `last_login` hash.

    Attributes:
        market_data_token (str | None): The market data token in force now.
        interactive_user_id (str | None): The user id inside the interactive token in force now.
        interactive_session (str | None): The session inside the interactive token in force now.
        login_count (int): How many interactive logins the stand-in API has made.
        market_data_logins (int): How many market data logins the stand-in API has made.
        market_data_fails (bool): Whether the market data login fails.
    """

    market_data_token = None
    interactive_user_id = 'WC0001'
    interactive_session = None
    login_count = 0
    market_data_logins = 0
    market_data_fails = False


class StandInWisdomCapitalAPI:
    """A stand-in for `WisdomCapitalAPI` whose construction is a pretend login to both XTS applications.

    Attributes:
        market_data_session_error (Exception | None): The market data login failure, recorded rather than raised, as the real class does.
    """

    def __init__(self):
        """Logs in, which here only issues numbered tokens.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.login_count = LoginRecord.login_count + 1
        LoginRecord.interactive_session = f'interactive-{LoginRecord.login_count}'
        print(f'Stand-in login number {LoginRecord.login_count}')
        self.market_data_session_error = None
        if LoginRecord.market_data_fails:
            self.market_data_session_error = RuntimeError('the market data login was refused: Invalid secret key')
        elif LoginRecord.market_data_token is None:
            self.log_in_to_market_data()

    def log_in_to_market_data(self):
        """Issues the next numbered market data token.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.market_data_logins = LoginRecord.market_data_logins + 1
        LoginRecord.market_data_token = f'market-data-token-{LoginRecord.market_data_logins}'

    def _current_login(self):
        """The interactive login in force now, its token a JWT carrying the user id as XTS signs it.

        Returns:
            dict: The access token.
        """
        if LoginRecord.interactive_session is None:
            return {
                'access_token': None,
            }
        claims = {
            'session': LoginRecord.interactive_session,
        }
        if LoginRecord.interactive_user_id is not None:
            claims['userID'] = LoginRecord.interactive_user_id
        payload = base64.urlsafe_b64encode(json.dumps(claims).encode()).decode().rstrip('=')
        return {
            'access_token': f'header.{payload}.signature',
        }

    def market_data_session(self):
        """The market data token and user id in force now.

        Returns:
            dict: `access_token` and `user_id`.
        """
        return {
            'access_token': LoginRecord.market_data_token,
            'user_id': 'WCMD01',
        }

    def replace_market_data_session(self, stale_access_token=None):
        """Logs in to market data again, unless the token in force is no longer the stale one.

        Args:
            stale_access_token (str | None): The token that was refused.

        Returns:
            dict: The market data session now in force.
        """
        if LoginRecord.market_data_token == stale_access_token:
            self.log_in_to_market_data()
            print(f'Stand-in market data login issued {LoginRecord.market_data_token}')
        else:
            print('The market data token was already replaced; no login.')
        return self.market_data_session()


class PinnedResponse:
    """A stand-in for the `urllib3` response of one XTS request.

    Attributes:
        status (int): The HTTP status.
        data (bytes): The body.
    """

    def __init__(self, status, text):
        """Keeps the answer.

        Args:
            status (int): The HTTP status.
            text (str): The body as text.

        Returns:
            None: This method returns nothing.
        """
        self.status = status
        self.data = text.encode()


class PinnedPool:
    """A stand-in for `urllib3.HTTPSConnectionPool` that answers from a shared script.

    Attributes:
        answers (list): The answers still to give, shared by every pool.
    """

    def __init__(self, answers):
        """Keeps the shared answers.

        Args:
            answers (list): The answers still to give, shared by every pool.

        Returns:
            None: This method returns nothing.
        """
        self.answers = answers

    def request(self, method, path, body=None, headers=None, timeout=None):
        """Prints the request and gives the next scripted answer.

        Args:
            method (str): The HTTP method.
            path (str): The path, with its query string.
            body (str | None): The JSON body.
            headers (dict | None): The request headers.
            timeout (int | None): The request timeout in seconds.

        Returns:
            PinnedResponse: The answer.
        """
        shown = path.split('?')[0]
        if body is not None:
            code = json.loads(body).get('xtsMessageCode')
            shown = f'{shown} for message code {code}'
        answer = self.answers.pop(0)
        print(f'HTTPS {method} {shown}: HTTP {answer.status}')
        return answer


class StandInUrllib3:
    """A stand-in for the `urllib3` package whose pools answer from one script.

    Attributes:
        answers (list): The answers, in the order requests are made.
    """

    def __init__(self, answers):
        """Keeps the answers.

        Args:
            answers (list): The answers, in the order requests are made.

        Returns:
            None: This method returns nothing.
        """
        self.answers = answers

    def HTTPSConnectionPool(self, host, port, assert_fingerprint=None):
        """Builds a pool, with the same signature as the real class.

        Args:
            host (str): The host.
            port (int): The port.
            assert_fingerprint (str | None): The certificate pin.

        Returns:
            PinnedPool: The pool.
        """
        return PinnedPool(self.answers)


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`, where a frame may also be a function to call at that point.
        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.split('?')[0]} with Engine.IO session {self.url.split('sid=')[1].split('&')[0]}")
        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
            if callable(frame):
                frame()
                continue
            self.handlers['on_message'](self, frame)
        self.handlers['on_close'](self, 1000, 'normal closure')

    def send(self, data, opcode=None):
        """Prints a message the socket sends to the broker.

        Args:
            data (str | bytes): The message.
            opcode (int | None): The frame type, when the socket names one.

        Returns:
            None: This method returns nothing.
        """
        print(f'Sent: {data}')

    def close(self, **options):
        """Notes that the socket asked to close.

        Args:
            **options (dict): Any close status the socket passes.

        Returns:
            None: This method returns nothing.
        """
        self.closed = True


class ScriptedWebsocketModule:
    """A stand-in for the `websocket` package that hands out scripted connections in turn.

    Attributes:
        plans (list): What each connection does, in the order the socket connects.
        when_finished (callable | None): Called when the last connection is handed out, so the socket stops after it.
    """

    def __init__(self, plans):
        """Keeps the connection plans.

        Args:
            plans (list): What each connection does, in the order the socket connects.

        Returns:
            None: This method returns nothing.
        """
        self.plans = plans
        self.when_finished = None

    def WebSocketApp(self, url, **handlers):
        """Builds the next scripted connection, closing the socket after the last one.

        Args:
            url (str): The feed URL.
            **handlers (dict): The socket's `on_open`, `on_message`, `on_error` and `on_close` callbacks.

        Returns:
            ScriptedConnection: The connection.
        """
        plan = self.plans.pop(0)
        if not self.plans:
            self.when_finished()
        return ScriptedConnection(url, plan, handlers)


class SnapshotThenDepthExample:
    """Runs a Wisdom Capital quotes socket against a scripted handshake, subscription and connection.

    Attributes:
        socket (WisdomCapitalQuotesSocket): The socket being shown.
    """

    def __init__(self):
        """Builds the answers, the frames, the stand-ins, the session and the socket.

        Returns:
            None: This method returns nothing.
        """
        logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
        api_module = types.ModuleType('stock_brokers.api.wisdom_capital')
        api_module.WisdomCapitalAPI = StandInWisdomCapitalAPI
        sys.modules['stock_brokers.api.wisdom_capital'] = api_module
        answers = [
            self.handshake_answer('engine-session-1'),
            self.subscription_answer([
                self.touchline(2950.5),
            ]),
            self.subscription_answer([]),
        ]
        sys.modules['urllib3'] = StandInUrllib3(answers)
        plans = [
            {
                'frames': [
                    '3probe',
                    '2',
                    self.depth_event(2951.0),
                ],
            },
        ]
        websocket_module = ScriptedWebsocketModule(plans)
        sys.modules['websocket'] = websocket_module
        logger = logging.getLogger('wisdom_capital.quotes')
        tokens = [
            '1:2885',
        ]
        names = {
            '1:2885': 'NSE:RELIANCE',
        }
        session = WisdomCapitalMarketDataSession(logger)
        self.socket = WisdomCapitalQuotesSocket('socket_0', tokens, names, session, self.print_ticks, logger)
        websocket_module.when_finished = self.socket.close

    def handshake_answer(self, session_id):
        """An Engine.IO polling handshake answer, framed with its length prefix as XTS sends it.

        Args:
            session_id (str): The Engine.IO session id.

        Returns:
            PinnedResponse: The answer.
        """
        handshake = {
            'sid': session_id,
            'upgrades': [
                'websocket',
            ],
            'pingInterval': 25000,
            'pingTimeout': 20000,
        }
        text = json.dumps(handshake)
        return PinnedResponse(200, f'{len(text) + 1}:0{text}2:40')

    def touchline(self, last_price):
        """An XTS touchline for NSE RELIANCE (segment 1, instrument 2885), with XTS's misspelt last traded quantity.

        Args:
            last_price (float): The last traded price.

        Returns:
            dict: The touchline.
        """
        return {
            'ExchangeSegment': 1,
            'ExchangeInstrumentID': 2885,
            'LastTradedPrice': last_price,
            'LastTradedQunatity': 25,
            'TotalTradedQuantity': 1204500,
            'AverageTradedPrice': 2949.0,
            'Open': 2930.0,
            'High': 2960.0,
            'Low': 2925.0,
            'Close': 2910.0,
            'LastTradedTime': 1790311528 - 315513000,
            'ExchangeTimeStamp': 1790311529 - 315513000,
            'BidInfo': {
                'Size': 100,
                'Price': 2950.0,
                'TotalOrders': 3,
            },
            'AskInfo': {
                'Size': 120,
                'Price': 2950.5,
                'TotalOrders': 4,
            },
        }

    def depth_event(self, last_price):
        """A socket.io market depth event whose quote nests its touchline, with padded zero rows.

        Args:
            last_price (float): The last traded price.

        Returns:
            str: The frame.
        """
        bids = []
        asks = []
        for level in range(5):
            bids.append({
                'Size': 100 + level,
                'Price': 2950.0 - level * 0.05,
                'TotalOrders': 3,
            })
            asks.append({
                'Size': 200 + level,
                'Price': 2951.0 + level * 0.05,
                'TotalOrders': 4,
            })
        bids.append({
            'Size': 0,
            'Price': 0,
            'TotalOrders': 0,
        })
        quote = {
            'ExchangeSegment': 1,
            'ExchangeInstrumentID': 2885,
            'Bids': bids,
            'Asks': asks,
            'Touchline': self.touchline(last_price),
        }
        event = [
            '1502-json-full',
            json.dumps(quote),
        ]
        return '42' + json.dumps(event)

    def subscription_answer(self, quotes):
        """A successful subscription answer carrying the current snapshot as JSON strings.

        Args:
            quotes (list): The quotes in the snapshot.

        Returns:
            PinnedResponse: The answer.
        """
        encoded = []
        for quote in quotes:
            encoded.append(json.dumps(quote))
        answer = {
            'type': 'success',
            'result': {
                'listQuotes': encoded,
            },
        }
        return PinnedResponse(200, json.dumps(answer))

    def print_ticks(self, ticks):
        """Prints the main fields of each tick the socket handed on.

        Args:
            ticks (list): The ticks.

        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']}) on {tick['exchange']} in {tick['mode']} mode")
            print(f"  last_price={tick['last_price']} last_quantity={tick['last_quantity']} volume={tick['volume']} change={change}")
            print(f"  last_trade_time={tick['last_trade_time']} exchange_timestamp={tick['exchange_timestamp']}")
            if tick['depth']['buy']:
                print(f"  best bid={tick['depth']['buy'][0]} best offer={tick['depth']['sell'][0]} levels={len(tick['depth']['buy'])}")

    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__':
    SnapshotThenDepthExample().run()

It prints:

Stand-in login number 1
HTTPS GET /apimarketdata/socket.io/: HTTP 200
Connecting to wss://trade.wisdomcapital.in/apimarketdata/socket.io/ with Engine.IO session engine-session-1
Sent: 2probe
Sent: 5
HTTPS POST /apimarketdata/instruments/subscription for message code 1501: HTTP 200
HTTPS POST /apimarketdata/instruments/subscription for message code 1502: HTTP 200
Tick NSE:RELIANCE (1:2885) on NSECM in full mode
  last_price=2950.5 last_quantity=25 volume=1204500 change=1.3918
  last_trade_time=1790311528 exchange_timestamp=1790311529
  best bid={'quantity': 100, 'price': 2950.0, 'orders': 3} best offer={'quantity': 120, 'price': 2950.5, 'orders': 4} levels=1
INFO socket_0 subscribed to 1 instrument(s).
Sent: 3
Tick NSE:RELIANCE (1:2885) on NSECM in full mode
  last_price=2951.0 last_quantity=25 volume=1204500 change=1.4089
  last_trade_time=1790311528 exchange_timestamp=1790311529
  best bid={'quantity': 100, 'price': 2950.0, 'orders': 3} best offer={'quantity': 200, 'price': 2951.0, 'orders': 4} levels=5
INFO socket_0 closed with status 1000: normal closure
INFO socket_0 stopped.
Gave up: False
examples/stock_brokers/websockets/wisdom_capital/WisdomCapitalQuotesSocket/example_2_refused_token_and_stale_subscription.py
"""Shows a Wisdom Capital quotes socket replacing a refused market data token and clearing a subscription left from an earlier connection.

XTS refuses a dead market data token at the Engine.IO handshake with an answer mentioning `Invalid Token`. `WisdomCapitalQuotesSocket` takes that as a refused login, and its reconnect loop asks `WisdomCapitalMarketDataSession.replace` for a new token, which the API issues only if nobody replaced the refused one already. Separately, XTS answers a subscription for instruments still subscribed from an earlier connection with `e-session-0002 Instrument Already Subscribed`, whose prefix looks like a refused token; the socket instead drops that subscription with a `PUT` and asks again.

This program's first handshake is refused. The second succeeds, its touchline subscription is refused as already subscribed, then dropped and granted, and the depth subscription is granted. No real socket or HTTPS request is made: stand-in `urllib3` and `websocket` packages play the answers and the frames, and a stand-in `stock_brokers.api.wisdom_capital` module issues numbered market data tokens. The backoff is set to zero seconds.

Notice the market data login between the two handshakes, the `PUT` that drops the stale subscription, and the snapshot tick handed on once the subscription is granted.

Run it from the project root:

    python examples/stock_brokers/websockets/wisdom_capital/WisdomCapitalQuotesSocket/example_2_refused_token_and_stale_subscription.py
"""

import base64
import json
import logging
import sys
import types

from stock_brokers.websockets.wisdom_capital import (
    WisdomCapitalMarketDataSession,
    WisdomCapitalQuotesSocket,
)


class LoginRecord:
    """The Wisdom Capital login state every stand-in API object reads, standing in for the Redis `last_login` hash.

    Attributes:
        market_data_token (str | None): The market data token in force now.
        interactive_user_id (str | None): The user id inside the interactive token in force now.
        interactive_session (str | None): The session inside the interactive token in force now.
        login_count (int): How many interactive logins the stand-in API has made.
        market_data_logins (int): How many market data logins the stand-in API has made.
        market_data_fails (bool): Whether the market data login fails.
    """

    market_data_token = None
    interactive_user_id = 'WC0001'
    interactive_session = None
    login_count = 0
    market_data_logins = 0
    market_data_fails = False


class StandInWisdomCapitalAPI:
    """A stand-in for `WisdomCapitalAPI` whose construction is a pretend login to both XTS applications.

    Attributes:
        market_data_session_error (Exception | None): The market data login failure, recorded rather than raised, as the real class does.
    """

    def __init__(self):
        """Logs in, which here only issues numbered tokens.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.login_count = LoginRecord.login_count + 1
        LoginRecord.interactive_session = f'interactive-{LoginRecord.login_count}'
        print(f'Stand-in login number {LoginRecord.login_count}')
        self.market_data_session_error = None
        if LoginRecord.market_data_fails:
            self.market_data_session_error = RuntimeError('the market data login was refused: Invalid secret key')
        elif LoginRecord.market_data_token is None:
            self.log_in_to_market_data()

    def log_in_to_market_data(self):
        """Issues the next numbered market data token.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.market_data_logins = LoginRecord.market_data_logins + 1
        LoginRecord.market_data_token = f'market-data-token-{LoginRecord.market_data_logins}'

    def _current_login(self):
        """The interactive login in force now, its token a JWT carrying the user id as XTS signs it.

        Returns:
            dict: The access token.
        """
        if LoginRecord.interactive_session is None:
            return {
                'access_token': None,
            }
        claims = {
            'session': LoginRecord.interactive_session,
        }
        if LoginRecord.interactive_user_id is not None:
            claims['userID'] = LoginRecord.interactive_user_id
        payload = base64.urlsafe_b64encode(json.dumps(claims).encode()).decode().rstrip('=')
        return {
            'access_token': f'header.{payload}.signature',
        }

    def market_data_session(self):
        """The market data token and user id in force now.

        Returns:
            dict: `access_token` and `user_id`.
        """
        return {
            'access_token': LoginRecord.market_data_token,
            'user_id': 'WCMD01',
        }

    def replace_market_data_session(self, stale_access_token=None):
        """Logs in to market data again, unless the token in force is no longer the stale one.

        Args:
            stale_access_token (str | None): The token that was refused.

        Returns:
            dict: The market data session now in force.
        """
        if LoginRecord.market_data_token == stale_access_token:
            self.log_in_to_market_data()
            print(f'Stand-in market data login issued {LoginRecord.market_data_token}')
        else:
            print('The market data token was already replaced; no login.')
        return self.market_data_session()


class PinnedResponse:
    """A stand-in for the `urllib3` response of one XTS request.

    Attributes:
        status (int): The HTTP status.
        data (bytes): The body.
    """

    def __init__(self, status, text):
        """Keeps the answer.

        Args:
            status (int): The HTTP status.
            text (str): The body as text.

        Returns:
            None: This method returns nothing.
        """
        self.status = status
        self.data = text.encode()


class PinnedPool:
    """A stand-in for `urllib3.HTTPSConnectionPool` that answers from a shared script.

    Attributes:
        answers (list): The answers still to give, shared by every pool.
    """

    def __init__(self, answers):
        """Keeps the shared answers.

        Args:
            answers (list): The answers still to give, shared by every pool.

        Returns:
            None: This method returns nothing.
        """
        self.answers = answers

    def request(self, method, path, body=None, headers=None, timeout=None):
        """Prints the request and gives the next scripted answer.

        Args:
            method (str): The HTTP method.
            path (str): The path, with its query string.
            body (str | None): The JSON body.
            headers (dict | None): The request headers.
            timeout (int | None): The request timeout in seconds.

        Returns:
            PinnedResponse: The answer.
        """
        shown = path.split('?')[0]
        if body is not None:
            code = json.loads(body).get('xtsMessageCode')
            shown = f'{shown} for message code {code}'
        answer = self.answers.pop(0)
        print(f'HTTPS {method} {shown}: HTTP {answer.status}')
        return answer


class StandInUrllib3:
    """A stand-in for the `urllib3` package whose pools answer from one script.

    Attributes:
        answers (list): The answers, in the order requests are made.
    """

    def __init__(self, answers):
        """Keeps the answers.

        Args:
            answers (list): The answers, in the order requests are made.

        Returns:
            None: This method returns nothing.
        """
        self.answers = answers

    def HTTPSConnectionPool(self, host, port, assert_fingerprint=None):
        """Builds a pool, with the same signature as the real class.

        Args:
            host (str): The host.
            port (int): The port.
            assert_fingerprint (str | None): The certificate pin.

        Returns:
            PinnedPool: The pool.
        """
        return PinnedPool(self.answers)


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`, where a frame may also be a function to call at that point.
        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.split('?')[0]} with Engine.IO session {self.url.split('sid=')[1].split('&')[0]}")
        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
            if callable(frame):
                frame()
                continue
            self.handlers['on_message'](self, frame)
        self.handlers['on_close'](self, 1000, 'normal closure')

    def send(self, data, opcode=None):
        """Prints a message the socket sends to the broker.

        Args:
            data (str | bytes): The message.
            opcode (int | None): The frame type, when the socket names one.

        Returns:
            None: This method returns nothing.
        """
        print(f'Sent: {data}')

    def close(self, **options):
        """Notes that the socket asked to close.

        Args:
            **options (dict): Any close status the socket passes.

        Returns:
            None: This method returns nothing.
        """
        self.closed = True


class ScriptedWebsocketModule:
    """A stand-in for the `websocket` package that hands out scripted connections in turn.

    Attributes:
        plans (list): What each connection does, in the order the socket connects.
        when_finished (callable | None): Called when the last connection is handed out, so the socket stops after it.
    """

    def __init__(self, plans):
        """Keeps the connection plans.

        Args:
            plans (list): What each connection does, in the order the socket connects.

        Returns:
            None: This method returns nothing.
        """
        self.plans = plans
        self.when_finished = None

    def WebSocketApp(self, url, **handlers):
        """Builds the next scripted connection, closing the socket after the last one.

        Args:
            url (str): The feed URL.
            **handlers (dict): The socket's `on_open`, `on_message`, `on_error` and `on_close` callbacks.

        Returns:
            ScriptedConnection: The connection.
        """
        plan = self.plans.pop(0)
        if not self.plans:
            self.when_finished()
        return ScriptedConnection(url, plan, handlers)


class RefusedTokenAndStaleSubscriptionExample:
    """Runs a Wisdom Capital quotes socket through a refused handshake and a stale subscription.

    Attributes:
        socket (WisdomCapitalQuotesSocket): The socket being shown.
    """

    def __init__(self):
        """Builds the answers, the frames, the stand-ins, the session and the socket.

        Returns:
            None: This method returns nothing.
        """
        logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
        api_module = types.ModuleType('stock_brokers.api.wisdom_capital')
        api_module.WisdomCapitalAPI = StandInWisdomCapitalAPI
        sys.modules['stock_brokers.api.wisdom_capital'] = api_module
        refused = {
            'type': 'error',
            'code': 'e-session-0001',
            'description': 'Invalid Token',
        }
        already_subscribed = {
            'type': 'error',
            'code': 'e-session-0002',
            'description': 'Instrument Already Subscribed !',
        }
        success = {
            'type': 'success',
        }
        answers = [
            PinnedResponse(400, json.dumps(refused)),
            self.handshake_answer('engine-session-2'),
            PinnedResponse(400, json.dumps(already_subscribed)),
            PinnedResponse(200, json.dumps(success)),
            self.subscription_answer([
                self.touchline(2950.5),
            ]),
            self.subscription_answer([]),
        ]
        sys.modules['urllib3'] = StandInUrllib3(answers)
        plans = [
            {
                'frames': [
                    '3probe',
                ],
            },
        ]
        websocket_module = ScriptedWebsocketModule(plans)
        sys.modules['websocket'] = websocket_module
        logger = logging.getLogger('wisdom_capital.quotes')
        tokens = [
            '1:2885',
        ]
        session = WisdomCapitalMarketDataSession(logger)
        self.socket = WisdomCapitalQuotesSocket('socket_0', tokens, {}, session, self.print_ticks, logger)
        self.socket.MIN_BACKOFF_SECONDS = 0
        websocket_module.when_finished = self.socket.close

    def handshake_answer(self, session_id):
        """An Engine.IO polling handshake answer, framed with its length prefix as XTS sends it.

        Args:
            session_id (str): The Engine.IO session id.

        Returns:
            PinnedResponse: The answer.
        """
        handshake = {
            'sid': session_id,
            'upgrades': [
                'websocket',
            ],
            'pingInterval': 25000,
            'pingTimeout': 20000,
        }
        text = json.dumps(handshake)
        return PinnedResponse(200, f'{len(text) + 1}:0{text}2:40')

    def touchline(self, last_price):
        """An XTS touchline for NSE RELIANCE (segment 1, instrument 2885), with XTS's misspelt last traded quantity.

        Args:
            last_price (float): The last traded price.

        Returns:
            dict: The touchline.
        """
        return {
            'ExchangeSegment': 1,
            'ExchangeInstrumentID': 2885,
            'LastTradedPrice': last_price,
            'LastTradedQunatity': 25,
            'TotalTradedQuantity': 1204500,
            'AverageTradedPrice': 2949.0,
            'Open': 2930.0,
            'High': 2960.0,
            'Low': 2925.0,
            'Close': 2910.0,
            'LastTradedTime': 1790311528 - 315513000,
            'ExchangeTimeStamp': 1790311529 - 315513000,
            'BidInfo': {
                'Size': 100,
                'Price': 2950.0,
                'TotalOrders': 3,
            },
            'AskInfo': {
                'Size': 120,
                'Price': 2950.5,
                'TotalOrders': 4,
            },
        }

    def depth_event(self, last_price):
        """A socket.io market depth event whose quote nests its touchline, with padded zero rows.

        Args:
            last_price (float): The last traded price.

        Returns:
            str: The frame.
        """
        bids = []
        asks = []
        for level in range(5):
            bids.append({
                'Size': 100 + level,
                'Price': 2950.0 - level * 0.05,
                'TotalOrders': 3,
            })
            asks.append({
                'Size': 200 + level,
                'Price': 2951.0 + level * 0.05,
                'TotalOrders': 4,
            })
        bids.append({
            'Size': 0,
            'Price': 0,
            'TotalOrders': 0,
        })
        quote = {
            'ExchangeSegment': 1,
            'ExchangeInstrumentID': 2885,
            'Bids': bids,
            'Asks': asks,
            'Touchline': self.touchline(last_price),
        }
        event = [
            '1502-json-full',
            json.dumps(quote),
        ]
        return '42' + json.dumps(event)

    def subscription_answer(self, quotes):
        """A successful subscription answer carrying the current snapshot as JSON strings.

        Args:
            quotes (list): The quotes in the snapshot.

        Returns:
            PinnedResponse: The answer.
        """
        encoded = []
        for quote in quotes:
            encoded.append(json.dumps(quote))
        answer = {
            'type': 'success',
            'result': {
                'listQuotes': encoded,
            },
        }
        return PinnedResponse(200, json.dumps(answer))

    def print_ticks(self, ticks):
        """Prints the main fields of each tick the socket handed on.

        Args:
            ticks (list): The ticks.

        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']}) on {tick['exchange']} in {tick['mode']} mode")
            print(f"  last_price={tick['last_price']} last_quantity={tick['last_quantity']} volume={tick['volume']} change={change}")
            print(f"  last_trade_time={tick['last_trade_time']} exchange_timestamp={tick['exchange_timestamp']}")
            if tick['depth']['buy']:
                print(f"  best bid={tick['depth']['buy'][0]} best offer={tick['depth']['sell'][0]} levels={len(tick['depth']['buy'])}")

    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'Market data logins: {LoginRecord.market_data_logins}')


if __name__ == '__main__':
    RefusedTokenAndStaleSubscriptionExample().run()

It prints:

Stand-in login number 1
HTTPS GET /apimarketdata/socket.io/: HTTP 400
ERROR socket_0 connection failed: RuntimeError: Engine.IO handshake failed with 400: {"type": "error", "code": "e-session-0001", "description": "Invalid Token"}
WARNING Replacing the Wisdom Capital market data token.
Stand-in market data login issued market-data-token-2
WARNING socket_0 disconnected. Reconnecting in 0 seconds.
HTTPS GET /apimarketdata/socket.io/: HTTP 200
Connecting to wss://trade.wisdomcapital.in/apimarketdata/socket.io/ with Engine.IO session engine-session-2
Sent: 2probe
Sent: 5
HTTPS POST /apimarketdata/instruments/subscription for message code 1501: HTTP 400
INFO socket_0: message code 1501 is still subscribed from an earlier connection. Dropping that subscription and asking again.
HTTPS PUT /apimarketdata/instruments/subscription for message code 1501: HTTP 200
HTTPS POST /apimarketdata/instruments/subscription for message code 1501: HTTP 200
HTTPS POST /apimarketdata/instruments/subscription for message code 1502: HTTP 200
Tick 1:2885 (1:2885) on NSECM in full mode
  last_price=2950.5 last_quantity=25 volume=1204500 change=1.3918
  last_trade_time=1790311528 exchange_timestamp=1790311529
  best bid={'quantity': 100, 'price': 2950.0, 'orders': 3} best offer={'quantity': 120, 'price': 2950.5, 'orders': 4} levels=1
INFO socket_0 subscribed to 1 instrument(s).
INFO socket_0 closed with status 1000: normal closure
INFO socket_0 stopped.
Gave up: False
Market data logins: 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/wisdom_capital.py
def __init__(self, name, tokens, names, session, on_ticks, logger):
    """
    Sets up a socket for one batch of instruments.

    Args:
        name (str): The connection's name, for the log.
        tokens (list[str]): The batch of `SEGMENT:EXCHANGEINSTRUMENTID` instrument tokens.
        names (dict[str, str]): Each token to the instrument's name, which becomes a tick's `id`.
        session (WisdomCapitalMarketDataSession): The shared market data login.
        on_ticks (callable): Called with each list of ticks that carry a price or a book, on this socket's thread.
        logger (logging.Logger): Where the connection reports.

    Returns:
        None: This method returns nothing.
    """
    super().__init__(name, logger)
    self._tokens = tokens
    self._names = names
    self._session = session
    self._on_ticks = on_ticks
    self._transport = WisdomCapitalTransport()
    self._token = None
    self._heartbeat_seconds = QUOTES_HEARTBEAT_SECONDS
    self._connection_closed = threading.Event()
    self._state = {}

_connect

_connect()

Opens an Engine.IO session with the market data token in force now, attaches the websocket and blocks until it closes.

Returns:

Type Description
None

This method returns nothing.

Raises:

Type Description
RuntimeError

When the Engine.IO handshake is refused.

Source code in stock_brokers/websockets/wisdom_capital.py
def _connect(self):
    """
    Opens an Engine.IO session with the market data token in force now, attaches the websocket and blocks until it closes.

    Returns:
        None: This method returns nothing.

    Raises:
        RuntimeError: When the Engine.IO handshake is refused.
    """
    import websocket

    session = self._session.market_data()
    self._token = session["access_token"]
    user_id = session["user_id"]
    query = f"token={self._token}&userID={user_id}&publishFormat=JSON&broadcastMode=Full"

    handshake, status, body = self._transport.handshake(MARKET_DATA_PATH, query)
    if handshake is None:
        self._authentication_rejected = self._transport.is_authentication_refusal(body)
        raise RuntimeError(f"Engine.IO handshake failed with {status}: {body[:200]}")
    self._heartbeat_seconds = self._transport.heartbeat_seconds(handshake, QUOTES_HEARTBEAT_SECONDS)

    self._connection_closed = threading.Event()
    self._websocket_application = websocket.WebSocketApp(
        self._transport.websocket_url(MARKET_DATA_PATH, handshake, query),
        on_open=self._on_open,
        on_message=self._on_message,
        on_error=self._on_error,
        on_close=self._on_close,
    )
    self._websocket_application.run_forever(sslopt=self._transport.ssl_options())

_log_in_again

_log_in_again()

Replaces the refused market data token, unless another socket or process already has.

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/wisdom_capital.py
def _log_in_again(self):
    """
    Replaces the refused market data token, unless another socket or process already has.

    Returns:
        None: This method returns nothing.
    """
    self._session.replace(self._token)

_on_open

_on_open(websocket_connection)

Checks the pinned certificate, then begins the Engine.IO upgrade.

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/wisdom_capital.py
def _on_open(self, websocket_connection):
    """
    Checks the pinned certificate, then begins the Engine.IO upgrade.

    Args:
        websocket_connection (websocket.WebSocketApp): The connection that opened.

    Returns:
        None: This method returns nothing.
    """
    fingerprint = self._transport.peer_fingerprint(websocket_connection)
    if fingerprint != CERTIFICATE_FINGERPRINT:
        self._logger.error(f"{self.name}: {HOST} presented certificate {fingerprint}, which does not match the pin. If Wisdom Capital has renewed, refresh CERTIFICATE_FINGERPRINT.")
        websocket_connection.close()
        return
    self._opened = True
    self._state = {}
    websocket_connection.send("2probe")

_on_error

_on_error(websocket_connection, 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/wisdom_capital.py
def _on_error(self, websocket_connection, error):
    """
    Reports an error.

    Args:
        websocket_connection (websocket.WebSocketApp): The connection that failed.
        error (Exception): The error.

    Returns:
        None: This method returns nothing.
    """
    self._logger.error(f"{self.name} error: {error}")

_on_close

_on_close(websocket_connection, status_code, message)

Stops this connection's heartbeat and 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.

required
message str | None

The close reason.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/wisdom_capital.py
def _on_close(self, websocket_connection, status_code, message):
    """
    Stops this connection's heartbeat and reports that the connection closed.

    Args:
        websocket_connection (websocket.WebSocketApp): The connection that closed.
        status_code (int | None): The close status.
        message (str | None): The close reason.

    Returns:
        None: This method returns nothing.
    """
    self._connection_closed.set()
    super()._on_close(websocket_connection, status_code, message)

_on_message

_on_message(websocket_connection, message)

Handles Engine.IO control frames and hands on the quotes each socket.io event carries.

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/wisdom_capital.py
def _on_message(self, websocket_connection, message):
    """
    Handles Engine.IO control frames and hands on the quotes each socket.io event carries.

    Args:
        websocket_connection (websocket.WebSocketApp): The connection the frame arrived on.
        message (bytes | str): The frame.

    Returns:
        None: This method returns nothing.
    """
    if isinstance(message, (bytes, bytearray)):
        message = message.decode("utf-8", errors="replace")
    if message == "3probe":
        websocket_connection.send("5")
        self._transport.start_heartbeat(
            websocket_connection,
            self._connection_closed,
            self._heartbeat_seconds,
            self._stop,
            f"wisdom_capital_quotes_{self.name}_heartbeat",
        )
        self._subscribe(websocket_connection)
        return
    if message == "2":
        websocket_connection.send("3")
        return
    ticks = []
    for token in self._apply_events(message):
        ticks.append(self._tick(token))
    self._hand_on(ticks)

_subscribe

_subscribe(websocket_connection)

Subscribes the batch over REST for touchline and depth, and hands on the snapshot the answer carries.

Parameters:

Name Type Description Default
websocket_connection WebSocketApp

The open connection, closed when the token is refused.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/wisdom_capital.py
def _subscribe(self, websocket_connection):
    """
    Subscribes the batch over REST for touchline and depth, and hands on the snapshot the answer carries.

    Args:
        websocket_connection (websocket.WebSocketApp): The open connection, closed when the token is refused.

    Returns:
        None: This method returns nothing.
    """
    instruments = []
    for token in self._tokens:
        instruments.append({
            "exchangeSegment": int(token.split(":")[0]),
            "exchangeInstrumentID": int(token.split(":")[1]),
        })
    seeded = set()
    for code in (TOUCHLINE, MARKET_DEPTH):
        response, body = self._ask_to_subscribe(instruments, code)
        if response.status >= 300 and ALREADY_SUBSCRIBED_MARKER in str(body).lower():
            self._logger.info(f"{self.name}: message code {code} is still subscribed from an earlier connection. Dropping that subscription and asking again.")
            self._transport.request(
                "PUT",
                f"{MARKET_DATA_PATH}/instruments/subscription",
                token=self._token,
                body={"instruments": instruments, "xtsMessageCode": code},
            )
            response, body = self._ask_to_subscribe(instruments, code)
        if response.status >= 300:
            if self._transport.is_authentication_refusal(body):
                self._logger.warning(f"{self.name}: subscription refused the market data token: {body[:200]}")
                self._authentication_rejected = True
                websocket_connection.close()
                return
            self._logger.warning(f"{self.name}: subscription for message code {code} returned {response.status}: {body[:200]}")
            continue
        try:
            quotes = (json.loads(body).get("result") or {}).get("listQuotes") or []
        except (ValueError, AttributeError):
            quotes = []
        for quote in quotes:
            if isinstance(quote, str):
                quote = json.loads(quote)
            token = self._apply_quote(quote)
            if token:
                seeded.add(token)
    ticks = []
    for token in seeded:
        ticks.append(self._tick(token))
    self._hand_on(ticks)
    self._logger.info(f"{self.name} subscribed to {len(self._tokens)} instrument(s).")

_ask_to_subscribe

_ask_to_subscribe(instruments, code)

Asks for one message code's subscription.

Parameters:

Name Type Description Default
instruments list[dict]

The batch as XTS wants it, segment and instrument id per entry.

required
code int

The XTS message code, touchline or market depth.

required

Returns:

Type Description
tuple

The response and its decoded body.

Source code in stock_brokers/websockets/wisdom_capital.py
def _ask_to_subscribe(self, instruments, code):
    """
    Asks for one message code's subscription.

    Args:
        instruments (list[dict]): The batch as XTS wants it, segment and instrument id per entry.
        code (int): The XTS message code, touchline or market depth.

    Returns:
        tuple: The response and its decoded body.
    """
    response = self._transport.request(
        "POST",
        f"{MARKET_DATA_PATH}/instruments/subscription",
        token=self._token,
        body={"instruments": instruments, "xtsMessageCode": code},
    )
    return response, response.data.decode("utf-8", errors="replace")

_apply_events

_apply_events(message)

Merges the quotes in a frame's socket.io event into their instruments' state.

Parameters:

Name Type Description Default
message str

The frame; only 42[...] event frames carry quotes.

required

Returns:

Type Description
list[str]

The tokens whose state the event updated, each once, in order.

Source code in stock_brokers/websockets/wisdom_capital.py
def _apply_events(self, message):
    """
    Merges the quotes in a frame's socket.io event into their instruments' state.

    Args:
        message (str): The frame; only `42[...]` event frames carry quotes.

    Returns:
        list[str]: The tokens whose state the event updated, each once, in order.
    """
    if not isinstance(message, str) or not message.startswith("42"):
        return []
    try:
        event = json.loads(message[2:])
    except json.JSONDecodeError:
        return []
    if not isinstance(event, list) or len(event) < 2:
        return []
    payload = event[1]
    if isinstance(payload, str):
        try:
            payload = json.loads(payload)
        except json.JSONDecodeError:
            return []
    quotes = payload
    if not isinstance(payload, list):
        quotes = [payload]
    tokens = []
    for quote in quotes:
        token = self._apply_quote(quote)
        if token and token not in tokens:
            tokens.append(token)
    return tokens

_apply_quote

_apply_quote(quote)

Merges an XTS quote into its instrument's state, a depth message's nested touchline included.

Parameters:

Name Type Description Default
quote object

A decoded XTS quote.

required

Returns:

Type Description
str | None

The instrument's token, or None for something that is not a quote.

Source code in stock_brokers/websockets/wisdom_capital.py
def _apply_quote(self, quote):
    """
    Merges an XTS quote into its instrument's state, a depth message's nested touchline included.

    Args:
        quote (object): A decoded XTS quote.

    Returns:
        str | None: The instrument's token, or None for something that is not a quote.
    """
    if not isinstance(quote, dict) or quote.get("ExchangeSegment") is None or quote.get("ExchangeInstrumentID") is None:
        return None
    token = f"{quote['ExchangeSegment']}:{quote['ExchangeInstrumentID']}"
    state = self._state.setdefault(token, {})
    state.update(quote)
    touchline = quote.get("Touchline")
    if isinstance(touchline, dict):
        state.update(touchline)
    return token

_tick

_tick(token)

Builds a normalized tick from an instrument's merged state.

Parameters:

Name Type Description Default
token str

The instrument's SEGMENT:EXCHANGEINSTRUMENTID.

required

Returns:

Type Description
dict

The tick.

Source code in stock_brokers/websockets/wisdom_capital.py
def _tick(self, token):
    """
    Builds a normalized tick from an instrument's merged state.

    Args:
        token (str): The instrument's `SEGMENT:EXCHANGEINSTRUMENTID`.

    Returns:
        dict: The tick.
    """
    state = self._state.get(token) or {}
    depth = {
        "buy": self._levels(state.get("Bids")),
        "sell": self._levels(state.get("Asks")),
    }
    if not depth["buy"] and isinstance(state.get("BidInfo"), dict):
        depth["buy"] = self._levels([state["BidInfo"]])
    if not depth["sell"] and isinstance(state.get("AskInfo"), dict):
        depth["sell"] = self._levels([state["AskInfo"]])
    last_price = state.get("LastTradedPrice")
    close = state.get("Close")
    segment = state.get("ExchangeSegment")
    exchange = SEGMENT_NAMES.get(segment)
    if exchange is None and segment is not None:
        exchange = str(segment)
    mode = "quote"
    if depth["buy"] or depth["sell"]:
        mode = "full"
    last_quantity = state.get("LastTradedQunatity")
    if last_quantity is None:
        last_quantity = state.get("LastTradedQuantity")
    change = None
    if last_price is not None and close:
        change = (last_price - close) * 100 / close
    return {
        "id": self._names.get(token) or token,
        "broker": "wisdom_capital",
        "instrument_token": token,
        "exchange": exchange,
        "mode": mode,
        "last_price": last_price,
        "last_quantity": last_quantity,
        "average_price": state.get("AverageTradedPrice"),
        "volume": state.get("TotalTradedQuantity"),
        "buy_quantity": state.get("TotalBuyQuantity"),
        "sell_quantity": state.get("TotalSellQuantity"),
        "ohlc": {
            "open": state.get("Open"),
            "high": state.get("High"),
            "low": state.get("Low"),
            "close": close,
        },
        "change": change,
        "oi": state.get("OpenInterest"),
        "oi_day_high": None,
        "oi_day_low": None,
        "last_trade_time": self._true_epoch(state.get("LastTradedTime")),
        "exchange_timestamp": self._true_epoch(state.get("ExchangeTimeStamp") or state.get("LastUpdateTime")),
        "depth": depth,
        "received_at": time.time(),
    }

_levels

_levels(rows)

Depth levels from an XTS bid or ask list, without the zero rows XTS pads the book with.

Parameters:

Name Type Description Default
rows list | None

The depth rows.

required

Returns:

Type Description
list[dict]

The levels.

Source code in stock_brokers/websockets/wisdom_capital.py
def _levels(self, rows):
    """
    Depth levels from an XTS bid or ask list, without the zero rows XTS pads the book with.

    Args:
        rows (list | None): The depth rows.

    Returns:
        list[dict]: The levels.
    """
    levels = []
    for row in rows or []:
        if isinstance(row, dict) and (row.get("Price") or row.get("Size")):
            levels.append({
                "quantity": row.get("Size"),
                "price": row.get("Price"),
                "orders": row.get("TotalOrders"),
            })
    return levels

_true_epoch

_true_epoch(timestamp)

An XTS timestamp on the true Unix epoch.

Parameters:

Name Type Description Default
timestamp int | None

The timestamp XTS sent.

required

Returns:

Type Description
int | None

The epoch, or None for a missing one.

Source code in stock_brokers/websockets/wisdom_capital.py
def _true_epoch(self, timestamp):
    """
    An XTS timestamp on the true Unix epoch.

    Args:
        timestamp (int | None): The timestamp XTS sent.

    Returns:
        int | None: The epoch, or None for a missing one.
    """
    if timestamp:
        return timestamp + EPOCH_OFFSET_SECONDS
    return None

_hand_on

_hand_on(ticks)

Hands on the ticks that carry a price or a book.

Parameters:

Name Type Description Default
ticks list[dict]

The ticks.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/wisdom_capital.py
def _hand_on(self, ticks):
    """
    Hands on the ticks that carry a price or a book.

    Args:
        ticks (list[dict]): The ticks.

    Returns:
        None: This method returns nothing.
    """
    worth_writing = []
    for tick in ticks:
        if tick["last_price"] is not None or tick["depth"]["buy"] or tick["depth"]["sell"]:
            worth_writing.append(tick)
    if worth_writing:
        self._on_ticks(worth_writing)

close

close()

Closes the connection and stops reconnecting.

Returns:

Type Description
None

This method returns nothing.

Example usage

From example_2_giving_up_after_a_second_refusal.py
self.closed_socket.close()
Source code in stock_brokers/websockets/base.py
def close(self):
    """
    Closes the connection and stops reconnecting.

    Returns:
        None: This method returns nothing.
    """
    self._stop.set()
    if self._websocket_application is not None:
        self._websocket_application.close()

run_forever

run_forever()

Connects, and reconnects with backoff, until closed or unable to recover.

Returns:

Type Description
None

This method returns nothing. gave_up says whether the socket stopped because it could not recover.

Example usage

From example_1_reconnecting_after_a_drop.py
self.socket.run_forever()
From example_2_giving_up_after_a_second_refusal.py
self.refused_socket.run_forever()
From example_2_giving_up_after_a_second_refusal.py
self.closed_socket.run_forever()
Source code in stock_brokers/websockets/base.py
def run_forever(self):
    """
    Connects, and reconnects with backoff, until closed or unable to recover.

    Returns:
        None: This method returns nothing. `gave_up` says whether the socket stopped because it could not recover.
    """
    backoff = self.MIN_BACKOFF_SECONDS
    failed_connects = 0
    logged_in_again = False
    while not self._stop.is_set():
        self._opened = False
        self._authentication_rejected = False
        try:
            self._connect()
        except Exception as exception:
            self._logger.error(f"{self.name} connection failed: {type(exception).__name__}: {exception}")
        if self._stop.is_set():
            break

        if self._opened and not self._authentication_rejected:
            failed_connects = 0
            logged_in_again = False
            backoff = self.MIN_BACKOFF_SECONDS
        else:
            failed_connects = failed_connects + 1

        if self._authentication_rejected or failed_connects >= self.MAX_FAILED_CONNECTS:
            if logged_in_again and self._gives_up_after_logging_in_again(failed_connects):
                self.gave_up = True
                self._logger.error(f"{self.name} still cannot connect after logging in again. Giving up.")
                break
            try:
                self._log_in_again()
            except Exception as exception:
                self.gave_up = True
                self._logger.error(f"{self.name} could not log in again: {type(exception).__name__}: {exception}")
                break
            failed_connects = 0
            logged_in_again = True

        self._logger.warning(f"{self.name} disconnected. Reconnecting in {backoff} seconds.")
        self._stop.wait(backoff)
        backoff = min(backoff * 2, self.MAX_BACKOFF_SECONDS)
    self._logger.info(f"{self.name} stopped.")

_gives_up_after_logging_in_again

_gives_up_after_logging_in_again(failed_connects)

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
def _gives_up_after_logging_in_again(self, failed_connects):
    """
    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.

    Args:
        failed_connects (int): How many connects in a row have failed since the last login.

    Returns:
        bool: True to give up now.
    """
    return True

WisdomCapitalOrderUpdatesSocket

WisdomCapitalOrderUpdatesSocket(session, on_updates, logger)

Bases: BrokerWebsocket

Wisdom Capital's XTS interactive socket, which carries the account's order and position events.

Sets up the order updates socket.

Parameters:

Name Type Description Default
session WisdomCapitalInteractiveSession

The shared interactive login.

required
on_updates callable

Called on this socket's thread with an event's orders, its positions and the datetime it was received, one of the two lists empty, each entry exactly as XTS sent it.

required
logger Logger

Where the connection reports.

required

Returns:

Type Description
None

This method returns nothing.

Example programs

examples/stock_brokers/websockets/wisdom_capital/WisdomCapitalOrderUpdatesSocket/example_1_orders_positions_and_trades.py
"""Joins Wisdom Capital's XTS interactive namespace and prints the order and position events it hands on.

`WisdomCapitalOrderUpdatesSocket` joins the interactive namespace with the token every Wisdom Capital process shares, and the user id read from that token's JWT payload, adding `apiType=INTERACTIVE` so XTS pushes the account's order, trade and position events. It hands on an order event's orders and a position event's positions, each exactly as XTS sent them, and passes over a trade event because the order event that follows carries the fill. It answers a server ping `2` with `3`.

This program scripts one connection delivering an open order, a trade, a position, a server ping and the order filled. No real socket or HTTPS request is made: a stand-in `urllib3` package answers the Engine.IO handshake, a stand-in `websocket` package plays the frames and prints what the socket sends, and a stand-in `stock_brokers.api.wisdom_capital` module lets the real `WisdomCapitalInteractiveSession` supply a JWT token without logging in.

Notice that the trade event leaves no line, and that orders and positions reach the callback in separate calls.

Run it from the project root:

    python examples/stock_brokers/websockets/wisdom_capital/WisdomCapitalOrderUpdatesSocket/example_1_orders_positions_and_trades.py
"""

import base64
import json
import logging
import sys
import types

from stock_brokers.websockets.wisdom_capital import (
    WisdomCapitalInteractiveSession,
    WisdomCapitalOrderUpdatesSocket,
)


class LoginRecord:
    """The Wisdom Capital login state every stand-in API object reads, standing in for the Redis `last_login` hash.

    Attributes:
        market_data_token (str | None): The market data token in force now.
        interactive_user_id (str | None): The user id inside the interactive token in force now.
        interactive_session (str | None): The session inside the interactive token in force now.
        login_count (int): How many interactive logins the stand-in API has made.
        market_data_logins (int): How many market data logins the stand-in API has made.
        market_data_fails (bool): Whether the market data login fails.
    """

    market_data_token = None
    interactive_user_id = 'WC0001'
    interactive_session = None
    login_count = 0
    market_data_logins = 0
    market_data_fails = False


class StandInWisdomCapitalAPI:
    """A stand-in for `WisdomCapitalAPI` whose construction is a pretend login to both XTS applications.

    Attributes:
        market_data_session_error (Exception | None): The market data login failure, recorded rather than raised, as the real class does.
    """

    def __init__(self):
        """Logs in, which here only issues numbered tokens.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.login_count = LoginRecord.login_count + 1
        LoginRecord.interactive_session = f'interactive-{LoginRecord.login_count}'
        print(f'Stand-in login number {LoginRecord.login_count}')
        self.market_data_session_error = None
        if LoginRecord.market_data_fails:
            self.market_data_session_error = RuntimeError('the market data login was refused: Invalid secret key')
        elif LoginRecord.market_data_token is None:
            self.log_in_to_market_data()

    def log_in_to_market_data(self):
        """Issues the next numbered market data token.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.market_data_logins = LoginRecord.market_data_logins + 1
        LoginRecord.market_data_token = f'market-data-token-{LoginRecord.market_data_logins}'

    def _current_login(self):
        """The interactive login in force now, its token a JWT carrying the user id as XTS signs it.

        Returns:
            dict: The access token.
        """
        if LoginRecord.interactive_session is None:
            return {
                'access_token': None,
            }
        claims = {
            'session': LoginRecord.interactive_session,
        }
        if LoginRecord.interactive_user_id is not None:
            claims['userID'] = LoginRecord.interactive_user_id
        payload = base64.urlsafe_b64encode(json.dumps(claims).encode()).decode().rstrip('=')
        return {
            'access_token': f'header.{payload}.signature',
        }

    def market_data_session(self):
        """The market data token and user id in force now.

        Returns:
            dict: `access_token` and `user_id`.
        """
        return {
            'access_token': LoginRecord.market_data_token,
            'user_id': 'WCMD01',
        }

    def replace_market_data_session(self, stale_access_token=None):
        """Logs in to market data again, unless the token in force is no longer the stale one.

        Args:
            stale_access_token (str | None): The token that was refused.

        Returns:
            dict: The market data session now in force.
        """
        if LoginRecord.market_data_token == stale_access_token:
            self.log_in_to_market_data()
            print(f'Stand-in market data login issued {LoginRecord.market_data_token}')
        else:
            print('The market data token was already replaced; no login.')
        return self.market_data_session()


class PinnedResponse:
    """A stand-in for the `urllib3` response of one XTS request.

    Attributes:
        status (int): The HTTP status.
        data (bytes): The body.
    """

    def __init__(self, status, text):
        """Keeps the answer.

        Args:
            status (int): The HTTP status.
            text (str): The body as text.

        Returns:
            None: This method returns nothing.
        """
        self.status = status
        self.data = text.encode()


class PinnedPool:
    """A stand-in for `urllib3.HTTPSConnectionPool` that answers from a shared script.

    Attributes:
        answers (list): The answers still to give, shared by every pool.
    """

    def __init__(self, answers):
        """Keeps the shared answers.

        Args:
            answers (list): The answers still to give, shared by every pool.

        Returns:
            None: This method returns nothing.
        """
        self.answers = answers

    def request(self, method, path, body=None, headers=None, timeout=None):
        """Prints the request and gives the next scripted answer.

        Args:
            method (str): The HTTP method.
            path (str): The path, with its query string.
            body (str | None): The JSON body.
            headers (dict | None): The request headers.
            timeout (int | None): The request timeout in seconds.

        Returns:
            PinnedResponse: The answer.
        """
        shown = path.split('?')[0]
        if body is not None:
            code = json.loads(body).get('xtsMessageCode')
            shown = f'{shown} for message code {code}'
        answer = self.answers.pop(0)
        print(f'HTTPS {method} {shown}: HTTP {answer.status}')
        return answer


class StandInUrllib3:
    """A stand-in for the `urllib3` package whose pools answer from one script.

    Attributes:
        answers (list): The answers, in the order requests are made.
    """

    def __init__(self, answers):
        """Keeps the answers.

        Args:
            answers (list): The answers, in the order requests are made.

        Returns:
            None: This method returns nothing.
        """
        self.answers = answers

    def HTTPSConnectionPool(self, host, port, assert_fingerprint=None):
        """Builds a pool, with the same signature as the real class.

        Args:
            host (str): The host.
            port (int): The port.
            assert_fingerprint (str | None): The certificate pin.

        Returns:
            PinnedPool: The pool.
        """
        return PinnedPool(self.answers)


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`, where a frame may also be a function to call at that point.
        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.split('?')[0]} with Engine.IO session {self.url.split('sid=')[1].split('&')[0]}")
        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
            if callable(frame):
                frame()
                continue
            self.handlers['on_message'](self, frame)
        self.handlers['on_close'](self, 1000, 'normal closure')

    def send(self, data, opcode=None):
        """Prints a message the socket sends to the broker.

        Args:
            data (str | bytes): The message.
            opcode (int | None): The frame type, when the socket names one.

        Returns:
            None: This method returns nothing.
        """
        print(f'Sent: {data}')

    def close(self, **options):
        """Notes that the socket asked to close.

        Args:
            **options (dict): Any close status the socket passes.

        Returns:
            None: This method returns nothing.
        """
        self.closed = True


class ScriptedWebsocketModule:
    """A stand-in for the `websocket` package that hands out scripted connections in turn.

    Attributes:
        plans (list): What each connection does, in the order the socket connects.
        when_finished (callable | None): Called when the last connection is handed out, so the socket stops after it.
    """

    def __init__(self, plans):
        """Keeps the connection plans.

        Args:
            plans (list): What each connection does, in the order the socket connects.

        Returns:
            None: This method returns nothing.
        """
        self.plans = plans
        self.when_finished = None

    def WebSocketApp(self, url, **handlers):
        """Builds the next scripted connection, closing the socket after the last one.

        Args:
            url (str): The feed URL.
            **handlers (dict): The socket's `on_open`, `on_message`, `on_error` and `on_close` callbacks.

        Returns:
            ScriptedConnection: The connection.
        """
        plan = self.plans.pop(0)
        if not self.plans:
            self.when_finished()
        return ScriptedConnection(url, plan, handlers)


class OrdersPositionsAndTradesExample:
    """Runs the Wisdom Capital order update socket against one scripted connection.

    Attributes:
        socket (WisdomCapitalOrderUpdatesSocket): The socket being shown.
    """

    def __init__(self):
        """Builds the answers, the frames, the stand-ins, the session and the socket.

        Returns:
            None: This method returns nothing.
        """
        logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
        api_module = types.ModuleType('stock_brokers.api.wisdom_capital')
        api_module.WisdomCapitalAPI = StandInWisdomCapitalAPI
        sys.modules['stock_brokers.api.wisdom_capital'] = api_module
        sys.modules['urllib3'] = StandInUrllib3([
            self.handshake_answer('engine-session-1'),
        ])
        trade = [
            'trade',
            json.dumps({
                'AppOrderID': 1100012345,
                'LastTradedQuantity': 10,
            }),
        ]
        position = [
            'position',
            {
                'TradingSymbol': 'INFY',
                'Quantity': 10,
            },
        ]
        plans = [
            {
                'frames': [
                    '3probe',
                    self.order_event('New', 0),
                    '42' + json.dumps(trade),
                    '42' + json.dumps(position),
                    '2',
                    self.order_event('Filled', 10),
                ],
            },
        ]
        websocket_module = ScriptedWebsocketModule(plans)
        sys.modules['websocket'] = websocket_module
        logger = logging.getLogger('wisdom_capital.orders')
        self.socket = WisdomCapitalOrderUpdatesSocket(WisdomCapitalInteractiveSession(logger), self.print_updates, logger)
        websocket_module.when_finished = self.socket.close

    def handshake_answer(self, session_id):
        """An Engine.IO polling handshake answer, framed with its length prefix as XTS sends it.

        Args:
            session_id (str): The Engine.IO session id.

        Returns:
            PinnedResponse: The answer.
        """
        handshake = {
            'sid': session_id,
            'upgrades': [
                'websocket',
            ],
            'pingInterval': 25000,
            'pingTimeout': 20000,
        }
        text = json.dumps(handshake)
        return PinnedResponse(200, f'{len(text) + 1}:0{text}2:40')

    def order_event(self, status, filled_quantity):
        """A socket.io order event for an INFY buy, its payload a JSON string as XTS sends it.

        Args:
            status (str): XTS's order status.
            filled_quantity (int): How much has filled.

        Returns:
            str: The frame.
        """
        order = {
            'AppOrderID': 1100012345,
            'OrderStatus': status,
            'TradingSymbol': 'INFY',
            'OrderSide': 'BUY',
            'OrderQuantity': 10,
            'CumulativeQuantity': filled_quantity,
        }
        event = [
            'order',
            json.dumps(order),
        ]
        return '42' + json.dumps(event)

    def print_updates(self, orders, positions, received_at):
        """Prints each order and position the socket handed on.

        Args:
            orders (list): The event's orders, exactly as XTS sent them.
            positions (list): The event's positions, exactly as XTS sent them.
            received_at (datetime.datetime): When the event arrived, which is not printed.

        Returns:
            None: This method returns nothing.
        """
        for order in orders:
            print(f"Order {order['AppOrderID']} {order['OrderStatus']}: {order['OrderSide']} {order['CumulativeQuantity']} of {order['OrderQuantity']} {order['TradingSymbol']}")
        for position in positions:
            print(f"Position {position['TradingSymbol']}: net quantity {position['Quantity']}")

    def run(self):
        """Runs the socket's own 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__':
    OrdersPositionsAndTradesExample().run()

It prints:

Stand-in login number 1
HTTPS GET /interactive/socket.io/: HTTP 200
Connecting to wss://trade.wisdomcapital.in/interactive/socket.io/ with Engine.IO session engine-session-1
Sent: 2probe
Sent: 5
INFO Joined the interactive namespace. Waiting for order and position updates.
Order 1100012345 New: BUY 0 of 10 INFY
Position INFY: net quantity 10
Sent: 3
Order 1100012345 Filled: BUY 10 of 10 INFY
INFO Order updates socket closed with status 1000: normal closure
INFO Order updates socket stopped.
Gave up: False
examples/stock_brokers/websockets/wisdom_capital/WisdomCapitalOrderUpdatesSocket/example_2_logged_out_by_a_new_login.py
"""Shows Wisdom Capital's order update socket reconnecting with the new token after XTS logs it out.

When another login replaces the interactive token, XTS sends the old connection a `logout` event and then leaves it open while delivering nothing. `WisdomCapitalOrderUpdatesSocket` closes it, and its own `run_forever` waits for the replacing login to reach the shared `last_login` hash before reconnecting with it; only if the token is still the logged-out one after ten seconds does it treat the session as refused and log in again.

In this program another process has already stored its new login by the time the logout arrives, so the wait ends at once and no login is made here. The second connection delivers one order. No real socket or HTTPS request is made: stand-in `urllib3` and `websocket` packages play the answers and frames, and a stand-in `stock_brokers.api.wisdom_capital` module holds the shared login. The backoff is set to zero seconds.

Notice the logout warning, that the second handshake carries a different Engine.IO session, and that only one login was made in total.

Run it from the project root:

    python examples/stock_brokers/websockets/wisdom_capital/WisdomCapitalOrderUpdatesSocket/example_2_logged_out_by_a_new_login.py
"""

import base64
import json
import logging
import sys
import types

from stock_brokers.websockets.wisdom_capital import (
    WisdomCapitalInteractiveSession,
    WisdomCapitalOrderUpdatesSocket,
)


class LoginRecord:
    """The Wisdom Capital login state every stand-in API object reads, standing in for the Redis `last_login` hash.

    Attributes:
        market_data_token (str | None): The market data token in force now.
        interactive_user_id (str | None): The user id inside the interactive token in force now.
        interactive_session (str | None): The session inside the interactive token in force now.
        login_count (int): How many interactive logins the stand-in API has made.
        market_data_logins (int): How many market data logins the stand-in API has made.
        market_data_fails (bool): Whether the market data login fails.
    """

    market_data_token = None
    interactive_user_id = 'WC0001'
    interactive_session = None
    login_count = 0
    market_data_logins = 0
    market_data_fails = False


class StandInWisdomCapitalAPI:
    """A stand-in for `WisdomCapitalAPI` whose construction is a pretend login to both XTS applications.

    Attributes:
        market_data_session_error (Exception | None): The market data login failure, recorded rather than raised, as the real class does.
    """

    def __init__(self):
        """Logs in, which here only issues numbered tokens.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.login_count = LoginRecord.login_count + 1
        LoginRecord.interactive_session = f'interactive-{LoginRecord.login_count}'
        print(f'Stand-in login number {LoginRecord.login_count}')
        self.market_data_session_error = None
        if LoginRecord.market_data_fails:
            self.market_data_session_error = RuntimeError('the market data login was refused: Invalid secret key')
        elif LoginRecord.market_data_token is None:
            self.log_in_to_market_data()

    def log_in_to_market_data(self):
        """Issues the next numbered market data token.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.market_data_logins = LoginRecord.market_data_logins + 1
        LoginRecord.market_data_token = f'market-data-token-{LoginRecord.market_data_logins}'

    def _current_login(self):
        """The interactive login in force now, its token a JWT carrying the user id as XTS signs it.

        Returns:
            dict: The access token.
        """
        if LoginRecord.interactive_session is None:
            return {
                'access_token': None,
            }
        claims = {
            'session': LoginRecord.interactive_session,
        }
        if LoginRecord.interactive_user_id is not None:
            claims['userID'] = LoginRecord.interactive_user_id
        payload = base64.urlsafe_b64encode(json.dumps(claims).encode()).decode().rstrip('=')
        return {
            'access_token': f'header.{payload}.signature',
        }

    def market_data_session(self):
        """The market data token and user id in force now.

        Returns:
            dict: `access_token` and `user_id`.
        """
        return {
            'access_token': LoginRecord.market_data_token,
            'user_id': 'WCMD01',
        }

    def replace_market_data_session(self, stale_access_token=None):
        """Logs in to market data again, unless the token in force is no longer the stale one.

        Args:
            stale_access_token (str | None): The token that was refused.

        Returns:
            dict: The market data session now in force.
        """
        if LoginRecord.market_data_token == stale_access_token:
            self.log_in_to_market_data()
            print(f'Stand-in market data login issued {LoginRecord.market_data_token}')
        else:
            print('The market data token was already replaced; no login.')
        return self.market_data_session()


class PinnedResponse:
    """A stand-in for the `urllib3` response of one XTS request.

    Attributes:
        status (int): The HTTP status.
        data (bytes): The body.
    """

    def __init__(self, status, text):
        """Keeps the answer.

        Args:
            status (int): The HTTP status.
            text (str): The body as text.

        Returns:
            None: This method returns nothing.
        """
        self.status = status
        self.data = text.encode()


class PinnedPool:
    """A stand-in for `urllib3.HTTPSConnectionPool` that answers from a shared script.

    Attributes:
        answers (list): The answers still to give, shared by every pool.
    """

    def __init__(self, answers):
        """Keeps the shared answers.

        Args:
            answers (list): The answers still to give, shared by every pool.

        Returns:
            None: This method returns nothing.
        """
        self.answers = answers

    def request(self, method, path, body=None, headers=None, timeout=None):
        """Prints the request and gives the next scripted answer.

        Args:
            method (str): The HTTP method.
            path (str): The path, with its query string.
            body (str | None): The JSON body.
            headers (dict | None): The request headers.
            timeout (int | None): The request timeout in seconds.

        Returns:
            PinnedResponse: The answer.
        """
        shown = path.split('?')[0]
        if body is not None:
            code = json.loads(body).get('xtsMessageCode')
            shown = f'{shown} for message code {code}'
        answer = self.answers.pop(0)
        print(f'HTTPS {method} {shown}: HTTP {answer.status}')
        return answer


class StandInUrllib3:
    """A stand-in for the `urllib3` package whose pools answer from one script.

    Attributes:
        answers (list): The answers, in the order requests are made.
    """

    def __init__(self, answers):
        """Keeps the answers.

        Args:
            answers (list): The answers, in the order requests are made.

        Returns:
            None: This method returns nothing.
        """
        self.answers = answers

    def HTTPSConnectionPool(self, host, port, assert_fingerprint=None):
        """Builds a pool, with the same signature as the real class.

        Args:
            host (str): The host.
            port (int): The port.
            assert_fingerprint (str | None): The certificate pin.

        Returns:
            PinnedPool: The pool.
        """
        return PinnedPool(self.answers)


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`, where a frame may also be a function to call at that point.
        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.split('?')[0]} with Engine.IO session {self.url.split('sid=')[1].split('&')[0]}")
        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
            if callable(frame):
                frame()
                continue
            self.handlers['on_message'](self, frame)
        self.handlers['on_close'](self, 1000, 'normal closure')

    def send(self, data, opcode=None):
        """Prints a message the socket sends to the broker.

        Args:
            data (str | bytes): The message.
            opcode (int | None): The frame type, when the socket names one.

        Returns:
            None: This method returns nothing.
        """
        print(f'Sent: {data}')

    def close(self, **options):
        """Notes that the socket asked to close.

        Args:
            **options (dict): Any close status the socket passes.

        Returns:
            None: This method returns nothing.
        """
        self.closed = True


class ScriptedWebsocketModule:
    """A stand-in for the `websocket` package that hands out scripted connections in turn.

    Attributes:
        plans (list): What each connection does, in the order the socket connects.
        when_finished (callable | None): Called when the last connection is handed out, so the socket stops after it.
    """

    def __init__(self, plans):
        """Keeps the connection plans.

        Args:
            plans (list): What each connection does, in the order the socket connects.

        Returns:
            None: This method returns nothing.
        """
        self.plans = plans
        self.when_finished = None

    def WebSocketApp(self, url, **handlers):
        """Builds the next scripted connection, closing the socket after the last one.

        Args:
            url (str): The feed URL.
            **handlers (dict): The socket's `on_open`, `on_message`, `on_error` and `on_close` callbacks.

        Returns:
            ScriptedConnection: The connection.
        """
        plan = self.plans.pop(0)
        if not self.plans:
            self.when_finished()
        return ScriptedConnection(url, plan, handlers)


class LoggedOutByANewLoginExample:
    """Runs the Wisdom Capital order update socket through a logout and a reconnect.

    Attributes:
        socket (WisdomCapitalOrderUpdatesSocket): The socket being shown.
    """

    def __init__(self):
        """Builds the answers, the frames, the stand-ins, the session and the socket.

        Returns:
            None: This method returns nothing.
        """
        logging.basicConfig(stream=sys.stdout, format='%(levelname)s %(message)s', level=logging.INFO)
        api_module = types.ModuleType('stock_brokers.api.wisdom_capital')
        api_module.WisdomCapitalAPI = StandInWisdomCapitalAPI
        sys.modules['stock_brokers.api.wisdom_capital'] = api_module
        sys.modules['urllib3'] = StandInUrllib3([
            self.handshake_answer('engine-session-1'),
            self.handshake_answer('engine-session-2'),
        ])
        logout = [
            'logout',
            {
                'message': 'Logged in from another device',
            },
        ]
        plans = [
            {
                'frames': [
                    '3probe',
                    self.store_other_login,
                    '42' + json.dumps(logout),
                ],
            },
            {
                'frames': [
                    '3probe',
                    self.order_event('Filled', 10),
                ],
            },
        ]
        websocket_module = ScriptedWebsocketModule(plans)
        sys.modules['websocket'] = websocket_module
        logger = logging.getLogger('wisdom_capital.orders')
        self.socket = WisdomCapitalOrderUpdatesSocket(WisdomCapitalInteractiveSession(logger), self.print_updates, logger)
        self.socket.MIN_BACKOFF_SECONDS = 0
        websocket_module.when_finished = self.socket.close

    def handshake_answer(self, session_id):
        """An Engine.IO polling handshake answer, framed with its length prefix as XTS sends it.

        Args:
            session_id (str): The Engine.IO session id.

        Returns:
            PinnedResponse: The answer.
        """
        handshake = {
            'sid': session_id,
            'upgrades': [
                'websocket',
            ],
            'pingInterval': 25000,
            'pingTimeout': 20000,
        }
        text = json.dumps(handshake)
        return PinnedResponse(200, f'{len(text) + 1}:0{text}2:40')

    def order_event(self, status, filled_quantity):
        """A socket.io order event for an INFY buy, its payload a JSON string as XTS sends it.

        Args:
            status (str): XTS's order status.
            filled_quantity (int): How much has filled.

        Returns:
            str: The frame.
        """
        order = {
            'AppOrderID': 1100012345,
            'OrderStatus': status,
            'TradingSymbol': 'INFY',
            'OrderSide': 'BUY',
            'OrderQuantity': 10,
            'CumulativeQuantity': filled_quantity,
        }
        event = [
            'order',
            json.dumps(order),
        ]
        return '42' + json.dumps(event)

    def store_other_login(self):
        """Stores another process's login, as the login that logged the socket out would, just before XTS sends the logout.

        Returns:
            None: This method returns nothing.
        """
        LoginRecord.interactive_session = 'interactive-from-another-process'

    def print_updates(self, orders, positions, received_at):
        """Prints each order and position the socket handed on.

        Args:
            orders (list): The event's orders, exactly as XTS sent them.
            positions (list): The event's positions, exactly as XTS sent them.
            received_at (datetime.datetime): When the event arrived, which is not printed.

        Returns:
            None: This method returns nothing.
        """
        for order in orders:
            print(f"Order {order['AppOrderID']} {order['OrderStatus']}: {order['OrderSide']} {order['CumulativeQuantity']} of {order['OrderQuantity']} {order['TradingSymbol']}")
        for position in positions:
            print(f"Position {position['TradingSymbol']}: net quantity {position['Quantity']}")

    def run(self):
        """Runs the socket's own 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__':
    LoggedOutByANewLoginExample().run()

It prints:

Stand-in login number 1
HTTPS GET /interactive/socket.io/: HTTP 200
Connecting to wss://trade.wisdomcapital.in/interactive/socket.io/ with Engine.IO session engine-session-1
Sent: 2probe
Sent: 5
INFO Joined the interactive namespace. Waiting for order and position updates.
WARNING Wisdom Capital logged this socket out: {"message": "Logged in from another device"}. Reconnecting with the login now in force.
INFO Order updates socket closed with status 1000: normal closure
WARNING Order updates socket disconnected. Reconnecting in 0 seconds.
HTTPS GET /interactive/socket.io/: HTTP 200
Connecting to wss://trade.wisdomcapital.in/interactive/socket.io/ with Engine.IO session engine-session-2
Sent: 2probe
Sent: 5
INFO Joined the interactive namespace. Waiting for order and position updates.
Order 1100012345 Filled: BUY 10 of 10 INFY
INFO Order updates socket closed with status 1000: normal closure
INFO Order updates socket stopped.
Gave up: False
Logins made: 1

Methods:

Name Description
run_forever

Connects, and reconnects with backoff, until closed or unable to recover, waiting after a logout for the login that replaced this one.

close

Closes the connection and stops reconnecting.

Source code in stock_brokers/websockets/wisdom_capital.py
def __init__(self, session, on_updates, logger):
    """
    Sets up the order updates socket.

    Args:
        session (WisdomCapitalInteractiveSession): The shared interactive login.
        on_updates (callable): Called on this socket's thread with an event's orders, its positions and the `datetime` it was received, one of the two lists empty, each entry exactly as XTS sent it.
        logger (logging.Logger): Where the connection reports.

    Returns:
        None: This method returns nothing.
    """
    super().__init__("Order updates socket", logger)
    self._session = session
    self._on_updates = on_updates
    self._transport = WisdomCapitalTransport()
    self._logged_out = False
    self._token = None
    self._heartbeat_seconds = ORDER_UPDATES_HEARTBEAT_SECONDS
    self._connection_closed = threading.Event()

run_forever

run_forever()

Connects, and reconnects with backoff, until closed or unable to recover, waiting after a logout for the login that replaced this one.

This is BrokerWebsocket.run_forever with one step added: after XTS logs the socket out, it waits for the replacing login to reach Redis, and treats a token that is still the logged-out one as a refused session.

Returns:

Type Description
None

This method returns nothing. gave_up says whether the socket stopped because it could not recover.

Example usage

From example_1_orders_positions_and_trades.py
self.socket.run_forever()
From example_2_logged_out_by_a_new_login.py
self.socket.run_forever()
Source code in stock_brokers/websockets/wisdom_capital.py
def run_forever(self):
    """
    Connects, and reconnects with backoff, until closed or unable to recover, waiting after a logout for the login that replaced this one.

    This is `BrokerWebsocket.run_forever` with one step added: after XTS logs the socket out, it waits for the replacing login to reach Redis, and treats a token that is still the logged-out one as a refused session.

    Returns:
        None: This method returns nothing. `gave_up` says whether the socket stopped because it could not recover.
    """
    backoff = self.MIN_BACKOFF_SECONDS
    failed_connects = 0
    logged_in_again = False
    while not self._stop.is_set():
        self._opened = False
        self._authentication_rejected = False
        self._logged_out = False
        try:
            self._connect()
        except Exception as exception:
            self._logger.error(f"{self.name} connection failed: {type(exception).__name__}: {exception}")
        if self._stop.is_set():
            break

        if self._logged_out and not self._wait_for_replacement_login():
            if self._stop.is_set():
                break
            self._logger.warning("The stored Wisdom Capital token is still the one that was logged out.")
            self._authentication_rejected = True

        if self._opened and not self._authentication_rejected:
            failed_connects = 0
            logged_in_again = False
            backoff = self.MIN_BACKOFF_SECONDS
        else:
            failed_connects = failed_connects + 1

        if self._authentication_rejected or failed_connects >= self.MAX_FAILED_CONNECTS:
            if logged_in_again:
                self.gave_up = True
                self._logger.error(f"{self.name} still cannot connect after logging in again. Giving up.")
                break
            try:
                self._log_in_again()
            except Exception as exception:
                self.gave_up = True
                self._logger.error(f"{self.name} could not log in again: {type(exception).__name__}: {exception}")
                break
            failed_connects = 0
            logged_in_again = True

        self._logger.warning(f"{self.name} disconnected. Reconnecting in {backoff} seconds.")
        self._stop.wait(backoff)
        backoff = min(backoff * 2, self.MAX_BACKOFF_SECONDS)
    self._logger.info(f"{self.name} stopped.")

_wait_for_replacement_login

_wait_for_replacement_login()

Waits for the login that logged this socket out to reach the shared last_login hash.

Returns:

Type Description
bool

True when the shared login holds a token other than the one this connection used, and False when it still holds that token after REPLACEMENT_LOGIN_WAIT_SECONDS or the socket is stopped first.

Source code in stock_brokers/websockets/wisdom_capital.py
def _wait_for_replacement_login(self):
    """
    Waits for the login that logged this socket out to reach the shared `last_login` hash.

    Returns:
        bool: True when the shared login holds a token other than the one this connection used, and False when it still holds that token after `REPLACEMENT_LOGIN_WAIT_SECONDS` or the socket is stopped first.
    """
    deadline = time.monotonic() + REPLACEMENT_LOGIN_WAIT_SECONDS
    while not self._stop.is_set():
        if self._session.current_token() != self._token:
            return True
        if time.monotonic() >= deadline:
            return False
        self._stop.wait(1)
    return False

_connect

_connect()

Opens an Engine.IO session with the shared login, attaches the websocket and blocks until it closes.

Returns:

Type Description
None

This method returns nothing.

Raises:

Type Description
RuntimeError

No usable token is stored, or the Engine.IO handshake is refused.

Source code in stock_brokers/websockets/wisdom_capital.py
def _connect(self):
    """
    Opens an Engine.IO session with the shared login, attaches the websocket and blocks until it closes.

    Returns:
        None: This method returns nothing.

    Raises:
        RuntimeError: No usable token is stored, or the Engine.IO handshake is refused.
    """
    import websocket

    try:
        token, user_id = self._session.shared_login()
    except RuntimeError:
        self._authentication_rejected = True
        raise
    self._token = token
    query = f"token={token}&userID={user_id}&apiType=INTERACTIVE"

    handshake, status, body = self._transport.handshake(INTERACTIVE_PATH, query)
    if handshake is None:
        self._authentication_rejected = self._transport.is_authentication_refusal(body)
        raise RuntimeError(f"Engine.IO handshake failed with {status}: {body[:200]}")
    self._heartbeat_seconds = self._transport.heartbeat_seconds(handshake, ORDER_UPDATES_HEARTBEAT_SECONDS)

    self._connection_closed = threading.Event()
    self._websocket_application = websocket.WebSocketApp(
        self._transport.websocket_url(INTERACTIVE_PATH, handshake, query),
        on_open=self._on_open,
        on_message=self._on_message,
        on_error=self._on_error,
        on_close=self._on_close,
    )
    self._websocket_application.run_forever(sslopt=self._transport.ssl_options())

_log_in_again

_log_in_again()

Constructs the API again, without checking whether another process already replaced the token.

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/wisdom_capital.py
def _log_in_again(self):
    """
    Constructs the API again, without checking whether another process already replaced the token.

    Returns:
        None: This method returns nothing.
    """
    self._session.log_in_again_without_checking()

_on_open

_on_open(websocket_connection)

Checks the pinned certificate, then begins the Engine.IO upgrade.

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/wisdom_capital.py
def _on_open(self, websocket_connection):
    """
    Checks the pinned certificate, then begins the Engine.IO upgrade.

    Args:
        websocket_connection (websocket.WebSocketApp): The connection that opened.

    Returns:
        None: This method returns nothing.
    """
    fingerprint = self._transport.peer_fingerprint(websocket_connection)
    if fingerprint != CERTIFICATE_FINGERPRINT:
        self._logger.error(f"{HOST} presented certificate {fingerprint}, which does not match the pin. If Wisdom Capital has renewed, refresh CERTIFICATE_FINGERPRINT.")
        websocket_connection.close()
        return
    self._opened = True
    websocket_connection.send("2probe")

_on_error

_on_error(websocket_connection, 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/wisdom_capital.py
def _on_error(self, websocket_connection, error):
    """
    Reports an error.

    Args:
        websocket_connection (websocket.WebSocketApp): The connection that failed.
        error (Exception): The error.

    Returns:
        None: This method returns nothing.
    """
    self._logger.error(f"Order updates error: {error}")

_on_close

_on_close(websocket_connection, status_code, message)

Stops this connection's heartbeat and 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.

required
message str | None

The close reason.

required

Returns:

Type Description
None

This method returns nothing.

Source code in stock_brokers/websockets/wisdom_capital.py
def _on_close(self, websocket_connection, status_code, message):
    """
    Stops this connection's heartbeat and reports that the connection closed.

    Args:
        websocket_connection (websocket.WebSocketApp): The connection that closed.
        status_code (int | None): The close status.
        message (str | None): The close reason.

    Returns:
        None: This method returns nothing.
    """
    self._connection_closed.set()
    super()._on_close(websocket_connection, status_code, message)

_on_message

_on_message(websocket_connection, message)

Handles Engine.IO control frames and hands on the orders and positions each socket.io event carries.

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/wisdom_capital.py
def _on_message(self, websocket_connection, message):
    """
    Handles Engine.IO control frames and hands on the orders and positions each socket.io event carries.

    Args:
        websocket_connection (websocket.WebSocketApp): The connection the frame arrived on.
        message (bytes | str): The frame.

    Returns:
        None: This method returns nothing.
    """
    if isinstance(message, (bytes, bytearray)):
        message = message.decode("utf-8", errors="replace")
    if message == "3probe":
        websocket_connection.send("5")
        self._transport.start_heartbeat(
            websocket_connection,
            self._connection_closed,
            self._heartbeat_seconds,
            self._stop,
            "wisdom_capital_order_updates_heartbeat",
        )
        self._logger.info("Joined the interactive namespace. Waiting for order and position updates.")
        return
    if message == "2":
        websocket_connection.send("3")
        return
    if not isinstance(message, str) or not message.startswith("42"):
        return

    try:
        event = json.loads(message[2:])
    except json.JSONDecodeError:
        return
    if not isinstance(event, list) or not event:
        return
    name = str(event[0]).lower()
    payload = None
    if len(event) > 1:
        payload = event[1]
    if isinstance(payload, str):
        try:
            payload = json.loads(payload)
        except json.JSONDecodeError:
            pass
    if name == "logout":
        self._logger.warning(f"Wisdom Capital logged this socket out: {json.dumps(payload)[:200]}. Reconnecting with the login now in force.")
        self._logged_out = True
        websocket_connection.close()
        return

    items = payload
    if not isinstance(payload, list):
        items = [payload]
    entries = []
    for item in items:
        if isinstance(item, dict):
            entries.append(item)
    received_at = datetime.now()

    if "position" in name:
        if entries:
            self._on_updates([], entries, received_at)
    elif "trade" in name:
        self._logger.debug("Trade event received; the matching order event carries the fill.")
    elif "order" in name or "interactive" in name:
        if entries:
            self._on_updates(entries, [], received_at)
    else:
        self._logger.info(f"Event {event[0]!r}: {json.dumps(payload)[:200]}")

close

close()

Closes the connection and stops reconnecting.

Returns:

Type Description
None

This method returns nothing.

Example usage

From example_2_giving_up_after_a_second_refusal.py
self.closed_socket.close()
Source code in stock_brokers/websockets/base.py
def close(self):
    """
    Closes the connection and stops reconnecting.

    Returns:
        None: This method returns nothing.
    """
    self._stop.set()
    if self._websocket_application is not None:
        self._websocket_application.close()

_gives_up_after_logging_in_again

_gives_up_after_logging_in_again(failed_connects)

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
def _gives_up_after_logging_in_again(self, failed_connects):
    """
    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.

    Args:
        failed_connects (int): How many connects in a row have failed since the last login.

    Returns:
        bool: True to give up now.
    """
    return True