Files
cosec_automation/trading/zerodha_kite/controllers/async_zerodha_kite.py
T
khushalps 7fdd2411bb Squashed 'utils_v2/' content from commit 62600ef
git-subtree-dir: utils_v2
git-subtree-split: 62600ef57051e69587da18015b7b6840fdac3194
2025-11-14 17:00:28 +05:30

1713 lines
67 KiB
Python

"""
AUTHOR:
Khushal P Soonderji
DATE:
Friday, 27th Dec., 2024
OBJECTIVE:
To provide a way to interface with Zerodha's Kite API in an asynchronous manner. Zerodha provides a great client
library, but it works only in sync mode. Here we will try to build an asynchronous version of the same for
more advance use cases.
REFERENCES:
01. Official documentation: https://kite.trade/docs/connect/v3/
DOWNLOADS:
N/A
"""
# *****************************************************************************************************************
# ***** ****
# *** IMPORT ***
# ***** ****
# *****************************************************************************************************************
# To make sibling directories accessible for imports:
import sys
from oauthlib.uri_validate import segment
sys.path.append(".")
sys.path.append("..")
# System-level activities:
import io
import os
# My utils:
from utils_v2.string import json
from utils_v2.system import files
from utils_v2.security.hash import Hasher
from utils_v2.date_time import date_time
# Data models:
from utils_v2.trading.zerodha_kite.models.api_call import ZerodhaKiteApiResponse
from utils_v2.trading.zerodha_kite.models.auth_tokens import ZerodhaKiteAuthTokens
from utils_v2.trading.zerodha_kite.models.user_profile import ZerodhaKiteUserProfile
from utils_v2.trading.zerodha_kite.models.user_funds import ZerodhaKiteUserFunds
from utils_v2.trading.zerodha_kite.models.instruments import ZerodhaKiteInstrument
from utils_v2.trading.zerodha_kite.models.ticks import (
ZerodhaKiteTick,
ZerodhaKiteMarketDepth,
OneZerodhaKiteMarketDepth
)
# To make API calls:
import httpx
import websockets
# To work with binary data:
import struct
# To work with date and time:
import datetime
import pytz
# For working with datatypes:
from typing import Literal, List, Callable, Awaitable, Union, Any
# For working with tabulated data:
import pandas as pd
# For debugging:
from icecream import IceCreamDebugger
import inspect
# For asynchronous activities:
import asyncio
# *****************************************************************************************************************
# ***** ****
# *** MACROS / ONE-TIME INIT ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** VARIABLES ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** FUNCTIONS ***
# ***** ****
# *****************************************************************************************************************
# --- Nothing Yet
# *****************************************************************************************************************
# ***** ****
# *** CLASSES ***
# ***** ****
# *****************************************************************************************************************
class AsyncZerodhaKite:
# ┏┓┓ ┓┏
# ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏
# ┗┛┗┗┻┛┛ ┗┛┗┻┛ ┛
# Root URLs:
ROOT_API_URL = r"https://api.kite.trade"
ROOT_WEBSOCKET_URL = r"wss://ws.kite.trade"
# Error types. These are the strings sent by Zerodha in their responses when some error occurs. The field in which
# this value will be found is 'error_type'.
# Documentation:
# 01. https://kite.trade/docs/connect/v3/response-structure/
# 02. https://kite.trade/docs/connect/v3/exceptions/
ERROR_TYPE_TOKEN_EXCEPTION = "TokenException"
ERROR_TYPE_USER_EXCEPTION = "UserException"
ERROR_TYPE_ORDER_EXCEPTION = "OrderException"
ERROR_TYPE_INPUT_EXCEPTION = "InputException"
ERROR_TYPE_MARGIN_EXCEPTION = "MarginException"
ERROR_TYPE_HOLDING_EXCEPTION = "HoldingException"
ERROR_TYPE_NETWORK_EXCEPTION = "NetworkException"
ERROR_TYPE_DATA_EXCEPTION = "DataException"
ERROR_TYPE_GENERAL_EXCEPTION = "GeneralException"
# Products offered by Zerodha:
PRODUCT_MIS = "MIS"
PRODUCT_CNC = "CNC"
PRODUCT_NRML = "NRML"
PRODUCT_CO = "CO"
# Order types offered by Zerodha:
ORDER_TYPE_MARKET = "MARKET"
ORDER_TYPE_LIMIT = "LIMIT"
ORDER_TYPE_SLM = "SL-M"
ORDER_TYPE_SL = "SL"
# Varieties offered by Zerodha:
VARIETY_REGULAR = "regular"
VARIETY_CO = "co"
VARIETY_AMO = "amo"
VARIETY_ICEBERG = "iceberg"
VARIETY_AUCTION = "auction"
# Transaction types offered by Zerodha:
TRANSACTION_TYPE_BUY = "BUY"
TRANSACTION_TYPE_SELL = "SELL"
# Validity types offered by Zerodha:
VALIDITY_DAY = "DAY"
VALIDITY_IOC = "IOC"
VALIDITY_TTL = "TTL"
# Position types offered by Zerodha:
POSITION_TYPE_DAY = "day"
POSITION_TYPE_OVERNIGHT = "overnight"
# Exchanges supported by Zerodha:
EXCHANGE_NSE = "NSE"
EXCHANGE_BSE = "BSE"
EXCHANGE_NFO = "NFO"
EXCHANGE_CDS = "CDS"
EXCHANGE_BFO = "BFO"
EXCHANGE_MCX = "MCX"
EXCHANGE_BCD = "BCD"
EXCHANGE_INDICES = "INDICES"
EXCHANGE_MCXSX = "MCXSX"
EXCHANGE_BSECDS = "BSECDS"
# Needed for decoding binary tick updates:
EXCHANGE_NAME_TO_CODE_MAP = {
EXCHANGE_NSE: 1,
EXCHANGE_NFO: 2,
EXCHANGE_CDS: 3,
EXCHANGE_BSE: 4,
EXCHANGE_BFO: 5,
EXCHANGE_BCD: 6,
EXCHANGE_MCX: 7,
EXCHANGE_MCXSX: 8,
EXCHANGE_INDICES: 9,
EXCHANGE_BSECDS: 6, # ... Effectively the same as "BCD". Only for backward compatibility.
}
EXCHANGE_CODE_TO_NAME_MAP = {v: k for k, v in EXCHANGE_NAME_TO_CODE_MAP.items()}
# For datetime normalization:
EXCHANGE_TIMEZONE_MAP = {
EXCHANGE_NSE: "Asia/Kolkata",
EXCHANGE_NFO: "Asia/Kolkata",
EXCHANGE_CDS: "Asia/Kolkata",
EXCHANGE_BSE: "Asia/Kolkata",
EXCHANGE_BFO: "Asia/Kolkata",
EXCHANGE_BCD: "Asia/Kolkata",
EXCHANGE_MCX: "Asia/Kolkata",
EXCHANGE_MCXSX: "Asia/Kolkata",
EXCHANGE_INDICES: "Asia/Kolkata",
EXCHANGE_BSECDS: "Asia/Kolkata", # ... Effectively the same as "BCD". Only for backward compatibility.
}
# Margins segments offered by Zerodha:
MARGIN_EQUITY = "equity"
MARGIN_COMMODITY = "commodity"
# Order statuses indicated by Zerodha:
STATUS_COMPLETE = "COMPLETE"
STATUS_REJECTED = "REJECTED"
STATUS_CANCELLED = "CANCELLED"
# GTT order type offered by Zerodha:
GTT_TYPE_OCO = "two-leg"
GTT_TYPE_SINGLE = "single"
# GTT order status indicated by Zerodha:
GTT_STATUS_ACTIVE = "active"
GTT_STATUS_TRIGGERED = "triggered"
GTT_STATUS_DISABLED = "disabled"
GTT_STATUS_EXPIRED = "expired"
GTT_STATUS_CANCELLED = "cancelled"
GTT_STATUS_REJECTED = "rejected"
GTT_STATUS_DELETED = "deleted"
# Intervals (for historical data) supported by Zerodha:
INTERVAL_1_MIN = "minute"
INTERVAL_3_MIN = "3minute"
INTERVAL_5_MIN = "5minute"
INTERVAL_10_MIN = "10minute"
INTERVAL_15_MIN = "15minute"
INTERVAL_30_MIN = "30minute"
INTERVAL_1_HOUR = "60minute"
INTERVAL_1DAY = "day"
# User identifiers and access control:
_api_key = None
_api_secret = None
_request_token = None
_access_token = None
_checksum = None
_user_profile = None
# For the websocket:
_ws = None
_ws_first_connect = True
_ws_last_heartbeat = None
_ws_conn_semaphore = asyncio.Semaphore(1)
_ws_listen_semaphore = asyncio.Semaphore(1)
_ws_subscription_semaphore = asyncio.Semaphore(1)
_ws_listening_for_messages = False
_ws_listener_task = None
_ws_made_conn_attempts = 0
_ws_max_conn_attempts = 100
_ws_subscribed_list = {
# instrument_token: mode
}
# Available tick-streaming modes (on the websocket):
# DOCUMENTATION: https://kite.trade/docs/connect/v3/websocket/
MODE_FULL = "full" # ..... Received in 184 bytes.
MODE_QUOTE = "quote" # ... Received in 44 bytes.
MODE_LTP = "ltp" # ....... Received in 8 bytes.
# Websocket callbacks:
# GitHub: https://github.com/zerodha/pykiteconnect/blob/master/kiteconnect/ticker.py
_ws_on_connect = None # ........ When the very first connection happens.
_ws_on_no_connect = None # ..... When a connection attempt fails.
_ws_on_disconnect = None # ..... When an established connection is lost.
_ws_on_reconnect = None # ...... When a re-connection attempt succeeds.
_ws_on_no_reconnect = None # ... When all re-connection attempts fail.
_ws_on_data = None # ........... When any data (binary or text) is received.
_ws_on_ticks = None # .......... When ticks are received.
_ws_on_error = None # .......... When an error message is received.
_ws_on_order_update = None # ... When an order's update is received.
_ws_on_message = None # ........ When a general message is received.
# Exceptions:
REQUEST_TOKEN_MISSING_EXCEPTION = ValueError(r"ERR: Please set a request token first.")
ACCESS_TOKEN_MISSING_EXCEPTION = ValueError(r"ERR: No access token found. Please complete the login cycle.")
# ┏┓
# ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓
# ┗┛┗┛┛┗┛┗┛ ┗┻┗┗┗┛┛
def __init__(
self,
api_key: str,
api_secret: str,
http_client: httpx.AsyncClient = None,
on_token_exception: Callable[[str, Union[ZerodhaKiteUserProfile, None], str, dict, ZerodhaKiteApiResponse], Awaitable] = None,
on_user_exception: Callable[[str, Union[ZerodhaKiteUserProfile, None], str, dict, ZerodhaKiteApiResponse], Awaitable] = None,
on_order_exception: Callable[[str, Union[ZerodhaKiteUserProfile, None], str, dict, ZerodhaKiteApiResponse], Awaitable] = None,
on_input_exception: Callable[[str, Union[ZerodhaKiteUserProfile, None], str, dict, ZerodhaKiteApiResponse], Awaitable] = None,
on_margin_exception: Callable[[str, Union[ZerodhaKiteUserProfile, None], str, dict, ZerodhaKiteApiResponse], Awaitable] = None,
on_holding_exception: Callable[[str, Union[ZerodhaKiteUserProfile, None], str, dict, ZerodhaKiteApiResponse], Awaitable] = None,
on_network_exception: Callable[[str, Union[ZerodhaKiteUserProfile, None], str, dict, ZerodhaKiteApiResponse], Awaitable] = None,
on_data_exception: Callable[[str, Union[ZerodhaKiteUserProfile, None], str, dict, ZerodhaKiteApiResponse], Awaitable] = None,
on_general_exception: Callable[[str, Union[ZerodhaKiteUserProfile, None], str, dict, ZerodhaKiteApiResponse], Awaitable] = None,
on_undocumented_exception: Callable[[str, Union[ZerodhaKiteUserProfile, None], str, dict, ZerodhaKiteApiResponse], Awaitable] = None,
debug = True,
debug_prefix = "Kite | ",
debug_only_errors = True
):
"""
To initialize the instance of this Zerodha Kite connector.
NOTE: All exception-handling callbacks must be async functions. On the occurrence of the associated exception,
the callback will be called with the API key of the user, the profile of the user (or null), the error type,
the error JSON (dict), and the full API response.
:param api_key: The API key that identifies your app. Note that this doesn't change in the lifecycle of the app.
:param api_secret: The API secret to access your app. Note that this can be changed from the API portal. This is
meant to be kept more secure than the simple API key.
:param http_client: An instance of 'httpx' library's AsyncClient. If not given, one will be instantiated
internally. It is recommended that, for multi-bot use cases, you provide a common HTTP client from outside.
:param on_token_exception: Called when the access token has some problem. This could be due to a manual log-out,
natural expiry (timeout), or maybe because the user logged-in from elsewhere.
:param on_user_exception: Called when there is some error relating to the user's account.
:param on_order_exception: Called when there is an error is placing orders.
:param on_input_exception: Called when there are missing required fields, bad values, etc.
:param on_margin_exception: Called when there are insufficient funds for placing orders.
:param on_holding_exception: Called when there are insufficient holdings. Typically, when you try to sell and
instrument with insufficient held quantity.
:param on_network_exception: Called when there is a network error. Typically, when the API wasn't able to
communicate with the OMS (order management system).
:param on_data_exception: Called when there is an internal server error at Zerodha when their API was unable to
understand the response from the OMS.
:param on_general_exception: Called when there is an unclassified error.
:param on_undocumented_exception: Called when there is an error but that error isn't in Zerodha's official
documentation.
:param debug: Whether, or not, you would like to show debugging messages on the terminal.
:param debug_prefix: The prefix string to identify the debugging messages.
:param debug_only_errors: Whether you would like to show all debugging messages or just error messages.
"""
# Prepare the debugging utility:
self._debug_prefix = debug_prefix
self._printer = IceCreamDebugger(prefix = debug_prefix, includeContext = True)
if not debug: self._printer.disable()
self._debug_only_errors = debug_only_errors
# Accept the input configuration:
self._api_key = api_key
self._api_secret = api_secret
# Accept the awaitables on various exceptions:
self._on_token_exception = on_token_exception
self._on_user_exception = on_user_exception
self._on_order_exception = on_order_exception
self._on_input_exception = on_input_exception
self._on_margin_exception = on_margin_exception
self._on_holding_exception = on_holding_exception
self._on_network_exception = on_network_exception
self._on_data_exception = on_data_exception
self._on_general_exception = on_general_exception
self._on_undocumented_exception = on_undocumented_exception
# Accept/create an HTTP client to work with:
self.__http_client = http_client or httpx.AsyncClient(
limits = httpx.Limits(
max_connections = 100, # ............ Maximum number of connections allowed in the pool.
max_keepalive_connections = 50, # ... Maximum number of connections that can be kept alive.
),
timeout = httpx.Timeout(
pool = 120.0, # .... Time to wait for a free connection from the pool.
connect = 5.0, # ... Time to wait for establishing a connection to the server.
write = 10.0, # .... Time to wait for sending data.
read = 120.0 # ..... Time to wait for receiving data.
)
)
def enable_debug(self):
self._printer.enable()
def disable_debug(self):
self._printer.disable()
def debug_only_errors(self):
self._debug_only_errors = True
def debug_everything(self):
self._debug_only_errors = False
# ┏┓ •
# ┃┃┏┓┏┓┏┓┏┓┏┓╋┓┏┓┏
# ┣┛┛ ┗┛┣┛┗ ┛ ┗┗┗ ┛
# ┛
@property
def access_token_valid(self):
return True if self._access_token is not None else False
@property
def access_token_expired(self):
return False if self._access_token is not None else True
# ┏┓ ┓┏ ┓┓•
# ┣ ┏┓┏┓┏┓┏┓ ┣┫┏┓┏┓┏┫┃┓┏┓┏┓
# ┗┛┛ ┛ ┗┛┛ ┛┗┗┻┛┗┗┻┗┗┛┗┗┫
# ┛
async def _handle_error_actions(
self,
api_response: ZerodhaKiteApiResponse
) -> None:
"""
To handle some common actions when we encounter errors.
DOCUMENTATION:
01. https://kite.trade/docs/connect/v3/response-structure/
02. https://kite.trade/docs/connect/v3/exceptions/
:param api_response: The full client response after making an API call.
:return: None
"""
# Get the error type and error JSON.
# If there is not error type, exit immediately:
await api_response.note_error()
error_type = api_response.errorType
if error_type is None: return
error_json = await api_response.get_json()
# Figure put which action has to be called:
error_to_action_map = {
self.ERROR_TYPE_TOKEN_EXCEPTION: self._on_token_exception,
self.ERROR_TYPE_USER_EXCEPTION: self._on_user_exception,
self.ERROR_TYPE_ORDER_EXCEPTION: self._on_order_exception,
self.ERROR_TYPE_INPUT_EXCEPTION: self._on_input_exception,
self.ERROR_TYPE_MARGIN_EXCEPTION: self._on_margin_exception,
self.ERROR_TYPE_HOLDING_EXCEPTION: self._on_holding_exception,
self.ERROR_TYPE_NETWORK_EXCEPTION: self._on_network_exception,
self.ERROR_TYPE_DATA_EXCEPTION: self._on_data_exception,
self.ERROR_TYPE_GENERAL_EXCEPTION: self._on_general_exception
}
action = error_to_action_map.get(error_type, self._on_undocumented_exception)
# Call the action:
if not self._debug_only_errors: self._printer(error_type, action)
if asyncio.iscoroutinefunction(self._on_token_exception):
try: await self._on_token_exception(self._api_key, self._user_profile, error_type, error_json, api_response)
except Exception as exception: self._printer(exception)
# ┏┓┏┓┳ ┏┓ ┓┓•
# ┣┫┃┃┃ ┃ ┏┓┃┃┓┏┓┏┓
# ┛┗┣┛┻ ┗┛┗┻┗┗┗┛┗┗┫
# ┛
async def __get(
self,
url: str,
headers: dict = None,
params: dict = None
) -> ZerodhaKiteApiResponse:
"""
To call an API using the GET method.
:param url: The URL to call.
:param headers: The headers to pass.
:param params: The params to send in the query string itself.
:return: A structured response that includes the raw response, the exception (if any), and so on.
"""
# Prepare the structure of the response:
api_response = ZerodhaKiteApiResponse(
action = inspect.stack()[1].function,
url = url,
method = "GET"
)
try:
# Make the API call:
response = await self.__http_client.get(
url = url,
headers = headers,
params = params
)
# Note down the results:
api_response.response = response
api_response.httpCode = response.status_code
api_response.message = response.reason_phrase
# Handle errors:
await self._handle_error_actions(api_response)
# If something goes wrong:
except Exception as exception:
api_response.exception = exception
api_response.message = str(exception)
self._printer(exception, api_response.url, api_response.method, headers, params)
# Done here:
return api_response
async def __post(
self,
url: str,
headers: dict = None,
json: dict = None,
data: dict = None,
params: dict = None,
content: str | bytes = None,
) -> ZerodhaKiteApiResponse:
"""
To call an API using the POST method.
:param url: The URL to call.
:param headers: The headers to pass.
:param json: The params to send in the JSON body.
:param data: The params to send in the form-data in the body.
:param params: The params to send in the query string itself.
:param content: The raw content to be sent in the body (typically as an octet-stream).
:return: A structured response that includes the raw response, the exception (if any), and so on.
"""
# Prepare the structure of the response:
api_response = ZerodhaKiteApiResponse(
action = inspect.stack()[1].function,
url = url,
method = "POST"
)
try:
# Make the API call:
response = await self.__http_client.post(
url = url,
headers = headers,
json = json,
data = data,
params = params,
content = content
)
# Note down the results:
api_response.response = response
api_response.httpCode = response.status_code
api_response.message = response.reason_phrase
# Handle errors:
await self._handle_error_actions(api_response)
# If something goes wrong:
except Exception as exception:
api_response.exception = exception
api_response.message = str(exception)
self._printer(exception, api_response.url, api_response.method, headers, json, data)
# Done here:
return api_response
async def __delete(
self,
url: str,
headers: dict = None,
params: dict = None,
) -> ZerodhaKiteApiResponse:
"""
To call an API using the DELETE method.
:param url: The URL to call.
:param headers: The headers to pass.
:param params: The params to send in the query string itself.
:return: A structured response that includes the raw response, the exception (if any), and so on.
"""
# Prepare the structure of the response:
api_response = ZerodhaKiteApiResponse(
action = inspect.stack()[1].function,
url = url,
method = "DELETE"
)
try:
# Make the API call:
response = await self.__http_client.delete(
url = url,
headers = headers,
params = params
)
# Note down the results:
api_response.response = response
api_response.httpCode = response.status_code
api_response.message = response.reason_phrase
# Handle errors:
await self._handle_error_actions(api_response)
# If something goes wrong:
except Exception as exception:
api_response.exception = exception
api_response.message = str(exception)
self._printer(exception, api_response.url, api_response.method, headers, params)
# Done here:
return api_response
# ┳┳ ┏┓ ┏┓ •
# ┃┃┏┏┓┏┓ ┣╋ ┗┓┏┓┏┏┓┏┓┏┓
# ┗┛┛┗ ┛ ┗┻ ┗┛┗ ┛┛┗┗┛┛┗
@property
def login_url(self) -> str:
"""
Generate the login URL that the user can use to log in to his Zerodha account for this app.
DOCUMENTATION:
01. https://kite.trade/docs/connect/v3/user/
:return: The login URL that the user must use.
"""
return f"https://kite.zerodha.com/connect/login?api_key={self._api_key}"
def set_request_token(
self,
request_token: str
) -> None:
"""
When the user authorizes the login flow, Zerodha's serve will send you a GET request on the callback URL that
you set on the PI portal for your app. This callback will have, among other things, a 'request_token'. The
request token is valid only for a very short period, and must be used to get a longer token called
'access_token' for actual activities. A checksum is needed for verification. Read about it in the official
documentation on Kite's API docs.
DOCUMENTATION:
01. https://kite.trade/docs/connect/v3/user/
:param request_token: The request token received from Zerodha when the user logs in.
:return: None.
"""
self._request_token = request_token
hasher = Hasher()
hasher.update(self._api_key + request_token + self._api_secret)
self._checksum = hasher.hexdigest()
async def generate_session(self) -> ZerodhaKiteAuthTokens | None:
"""
Once we have the 'request_token' from Zerodha's callback, we must generate a session by fetching an access
token. The access token will be used to perform most of the actual activities.
DOCUMENTATION:
01. https://kite.trade/docs/connect/v3/user/
:return: Either the auth-token model of Zerodha, or null if the process failed.
"""
# If we don't have a request token:
if not self._request_token: raise self.REQUEST_TOKEN_MISSING_EXCEPTION
# Start by assuming failure:
session = None
# Try to get a session from Zerodha:
client_response = await self.__post(
url = f"{self.ROOT_API_URL}/session/token",
headers = {"X-Kite-Version": "3"},
data = {
"api_key": self._api_key,
"request_token": self._request_token,
"checksum": self._checksum
}
)
# If the API call was successful, we have a valid session:
if client_response.success:
client_json = await client_response.get_json()
client_json = client_json["data"]
self.set_access_token(client_json["access_token"])
session = ZerodhaKiteAuthTokens(**client_json)
self._user_profile = ZerodhaKiteUserProfile(**client_json)
# Done here:
return session
def set_access_token(
self,
access_token: str
) -> None:
"""
Use this when you already have an access token from elsewhere. Maybe you had logged in earlier and stored the
granted access token in a database. Useful for other stateless deployments also.
:param access_token: The access token to do actual work like get ticks, and place orders.
:return: None
"""
self._access_token = access_token
async def logout(self) -> bool:
"""
When you want to log out from Zerodha Kite and destroy the access token so that it cannot be used anywhere.
:return: The structured API-call response.
"""
# If we don't have an access token:
if not self._access_token: raise self. ACCESS_TOKEN_MISSING_EXCEPTION
# Start by assuming failure:
success = False
# Try to get clear the session from Zerodha:
client_response = await self.__delete(
url = f"{self.ROOT_API_URL}/session/token",
headers = {"X-Kite-Version": "3"},
params = {
"api_key": self._api_key,
"access_token": self._access_token
}
)
# If the logout request was successful:
if client_response.success:
self._access_token = None
success = True
# Done here:
return success
async def get_user_profile(self) -> ZerodhaKiteUserProfile | None:
"""
This gives you the user's profile. The values are a subset of the values
:return: The structured response for the user profile.
"""
# If we already have the user's profile:
if self._user_profile is not None: return self._user_profile
# If we don't have an access token:
if not self._access_token: raise self.ACCESS_TOKEN_MISSING_EXCEPTION
# Start by assuming failure:
profile = None
# Make the API call:
client_response = await self.__get(
url = f"{self.ROOT_API_URL}/user/profile",
headers = {
"X-Kite-Version": "3",
"Authorization": f"token {self._api_key}:{self._access_token}"
}
)
# if the API call was successful:
if client_response.success:
profile_json = await client_response.get_json()
profile = ZerodhaKiteUserProfile(**profile_json["data"])
self._user_profile = profile
# Done here:
return profile
async def get_user_funds(self) -> ZerodhaKiteUserFunds | None:
"""
To get a detailed view of the funds/margins available in this user's account.
:return: A structured response that describes the status of the account's funds.
"""
# If we don't have an access token:
if not self._access_token: raise self.ACCESS_TOKEN_MISSING_EXCEPTION
# Start by assuming failure:
funds = None
# Make the API call:
client_response = await self.__get(
url = f"{self.ROOT_API_URL}/user/margins",
headers = {
"X-Kite-Version": "3",
"Authorization": f"token {self._api_key}:{self._access_token}"
}
)
# if the API call was successful:
if client_response.success:
funds_json = await client_response.get_json()
funds = ZerodhaKiteUserFunds(**funds_json["data"])
# Done here:
return funds
# ┏┓ ┏ ┓•
# ┃┃┏┓┏┓╋╋┏┓┃┓┏┓
# ┣┛┗┛┛ ┗┛┗┛┗┗┗┛
async def get_user_holdings(self):
"""
To get a list of holdings that this user has in his account.
:return: ??
"""
# If we don't have an access token:
if not self._access_token: raise self.ACCESS_TOKEN_MISSING_EXCEPTION
# Start by assuming failure:
holdings = None
# Make the API call:
client_response = await self.__get(
url = f"{self.ROOT_API_URL}/portfolio/holdings",
headers = {
"X-Kite-Version": "3",
"Authorization": f"token {self._api_key}:{self._access_token}"
}
)
# if the API call was successful:
if client_response.success:
holdings_json = await client_response.get_json()
print("HOLDINGS:", json.to_string(holdings_json))
# funds = ZerodhaKiteUserFunds(**funds_json["data"])
# Done here:
return holdings
async def get_user_positions(self):
"""
To get a list of live positions that this user has in his account.
:return: ??
"""
# If we don't have an access token:
if not self._access_token: raise self.ACCESS_TOKEN_MISSING_EXCEPTION
# Start by assuming failure:
positions = None
# Make the API call:
client_response = await self.__get(
url = f"{self.ROOT_API_URL}/portfolio/positions",
headers = {
"X-Kite-Version": "3",
"Authorization": f"token {self._api_key}:{self._access_token}"
}
)
# if the API call was successful:
if client_response.success:
positions_json = await client_response.get_json()
print("POSITIONS:", json.to_string(positions_json))
# funds = ZerodhaKiteUserFunds(**funds_json["data"])
# Done here:
return positions
# ┳
# ┃┏┓┏╋┏┓┓┏┏┳┓┏┓┏┓╋┏
# ┻┛┗┛┗┛ ┗┻┛┗┗┗ ┛┗┗┛
async def list_instruments(self) -> List[ZerodhaKiteInstrument]:
"""
Get the list of all the instruments supported by Zerodha.
:return: A list of structured instrument models.
"""
# If we don't have an access token:
if not self._access_token: raise self.ACCESS_TOKEN_MISSING_EXCEPTION
# Start by assuming failure:
instruments = []
# Make the API call:
client_response = await self.__http_client.get(
url = f"{self.ROOT_API_URL}/instruments",
headers = {
"X-Kite-Version": "3",
"Authorization": f"token {self._api_key}:{self._access_token}"
}
)
# if the API call was successful:
if client_response.status_code in [200]:
instruments = ZerodhaKiteInstrument.from_api_data(client_response.content)
# Done here:
return instruments
# ┓ ┏ ┓ ┓
# ┃┃┃┏┓┣┓┏┏┓┏┃┏┏┓╋
# ┗┻┛┗ ┗┛┛┗┛┗┛┗┗ ┗
@property
def websocket_is_connected(self):
"""
To check if we are connected to the websocket. As per the documentation of the library being used, you must
not call their 'status' property.
:return: True if connected, else False.
"""
if self._ws is None: return False
return True
@property
def websocket_latency(self) -> float | None:
"""
Useful for knowing the roundtrip time between your machine and Zerodha's server.
:return: The latency in seconds (if connected), else None.
"""
if self.websocket_is_connected: return self._ws.latency
return None
async def invoke_callback(self, callback, *args) -> None:
"""
Safely calls a callback.
:param callback: The asynchronous callable.
:param args: Any args to send to the callable.
:return: None.
"""
if callback is not None:
try: asyncio.create_task(callback(*args))
except Exception as exception: self._printer(callback.__name__, exception)
@staticmethod
def unpack_bytes(
binary_data: bytes,
start: int,
end: int,
byte_order: str = ">",
byte_format: str = "I"
) -> Any:
"""
To unpack binary data to the format of choice:
I --> Unsigned Int (32-bit)
H --> Unsigned Int (16-bit)
B --> Unsigned Int (8-bit)
i --> Signed Int (32-bit)
h --> Signed Int (16-bit)
b --> Signed Int (8-bit)
l --> Signed Long Int (32-bit)
q --> Signed Long Int (64-bit)
f --> Floating-Point (32-bit)
d --> Floating-Point (64-bit)
s --> String
p --> Pascal-Style String
c --> Char (8-bit)
? --> Bool (8-bit)
-----------------------------------------------
You can even select the byte-order:
= --> Native Byte Order
> --> Big-Endian (MSB-first)
< --> Little-Endian (LSB-first)
-----------------------------------------------
:param binary_data: The binary data
:param start: The starting index (inclusive).
:param end: The ending index (exclusive).
:param byte_order: The "endianness" of the data.
:param byte_format: The format in which to unpack
:return:
"""
return struct.unpack(byte_order + byte_format, binary_data[start:end])[0]
def split_packets(self, binary_data: bytes) -> List[bytes]:
"""
We split the full binary payload into individual packets here.
:param binary_data: The binary message (full) as it was received from Zerodha.
:return: A list of individual binary packets.
"""
# Ignore the heartbeats:
if len(binary_data) < 2: return []
# The first 2 bytes are the no. of packets that are in the message.
# Each 'packet' is the data of a tick:
packet_count = self.unpack_bytes(
binary_data = binary_data,
start = 0,
end = 2,
byte_order = ">",
byte_format = "H"
)
# Get the packets:
packets = []
offset = 2
for _ in range(packet_count):
packet_length = self.unpack_bytes(
binary_data = binary_data,
start = offset,
end = offset + 2,
byte_order = ">",
byte_format = "H"
)
packets.append(binary_data[offset + 2: offset + 2 + packet_length])
offset = offset + 2 + packet_length
# Done here:
return packets
def parse_exchange_ts(
self,
timestamp: float | int,
exchange: str,
to_timezone: str = date_time.TIMEZONE_UTC
) -> (datetime.datetime, str):
"""
Parses the raw timestamp and makes it timezone aware with the timezone reference of the mentioned exchange.
:param timestamp: The raw timestamp from Zerodha.
:param exchange: The name of the exchange to get an understanding of the timezone.
:param to_timezone: The timezone to which the output should be normalized.
:return: A tuple with the parsed datetime (in the tz mentioned in 'to_timezone') and the timezone string of the
exchange. Both can be null if parsing fails.
"""
# Start by assuming failure:
dt = tz = None
try:
# Parse the timestamp:
tz = self.EXCHANGE_TIMEZONE_MAP[exchange]
dt = datetime.datetime.fromtimestamp(timestamp)
dt = date_time.as_if_timezone(dt, timezone = tz)
dt = date_time.to_timezone(dt, timezone = to_timezone)
# In case of parsing failure:
except Exception as exception:
dt = tz = None
# Done here:
return dt, tz
def decode_ticks(self, binary_data: bytes) -> List[ZerodhaKiteTick]:
"""
To decode the binary payload of the ticks into a usable format.
DOCUMENTATION:
01. Official docs: https://kite.trade/docs/connect/v3/websocket/
02. GitHub: https://github.com/zerodha/pykiteconnect/blob/master/kiteconnect/ticker.py
:param binary_data: The full binary payload that contains multiple ticks in it.
:return: A list of structured ticks.
"""
# Split the full payload into individual packets:
packets = self.split_packets(binary_data = binary_data)
ticks = []
for packet in packets:
# Measure the size of the packet since
# it will decide how the data is decoded:
packet_size = len(packet)
# Extract the instrument's details:
instrument_token = self.unpack_bytes(
binary_data = packet,
start = 0,
end = 4,
byte_order = ">",
byte_format = "I"
)
exchange_code = instrument_token & 0xff
exchange_name = self.EXCHANGE_CODE_TO_NAME_MAP[exchange_code]
tradeable = False if exchange_name == self.EXCHANGE_INDICES else True
match exchange_name:
case self.EXCHANGE_CDS: divisor = 10_000_000.0
case self.EXCHANGE_BCD: divisor = 10_000.0
case _: divisor = 100.0
# Create an LTP Tick:
if packet_size == 8:
this_tick = ZerodhaKiteTick(
tickMode = self.MODE_LTP,
exchange = exchange_name,
instrumentToken = instrument_token,
tradeable = tradeable,
ltp = self.unpack_bytes(packet, start = 4, end = 8) / divisor
)
ticks.append(this_tick)
# print(json.to_string(this_tick.model_dump(), default=str))
# For Indices Quote and Full) mode:
elif packet_size == 28 or packet_size == 32:
# Compute basic values:
mode = self.MODE_QUOTE if len(packet) == 28 else self.MODE_FULL
prev_close = self.unpack_bytes(packet, start = 20, end = 24) / divisor
ltp = self.unpack_bytes(packet, start = 4, end = 8) / divisor
change = (ltp - prev_close)
pct_change = (change / prev_close) * 100.0
# Assume null for those things that will be given for only Full mode:
exchange_ts = None
exchange_tz = None
# If the Index tick is in Full mode:
if packet_size == 32:
exchange_ts, exchange_tz = self.parse_exchange_ts(
timestamp = self.unpack_bytes(packet, start = 28, end = 32),
exchange = exchange_name,
to_timezone = date_time.TIMEZONE_UTC
)
# Feed the model:
this_tick = ZerodhaKiteTick(
tickMode = mode,
exchange = exchange_name,
instrumentToken = instrument_token,
tradeable = tradeable,
prevClose = prev_close,
ltp = ltp,
o = self.unpack_bytes(packet, start = 16, end = 20) / divisor,
h = self.unpack_bytes(packet, start = 8, end = 12) / divisor,
l = self.unpack_bytes(packet, start = 12, end = 16) / divisor,
c = ltp,
chg = change,
pChg = pct_change,
exchgTs = exchange_ts,
exchgTz = exchange_tz,
)
ticks.append(this_tick)
# For Non-Index Quotes and Full packets:
elif packet_size == 44 or packet_size == 184:
# Compute basic values:
mode = self.MODE_QUOTE if len(packet) == 44 else self.MODE_FULL
prev_close = self.unpack_bytes(packet, start = 40, end = 44) / divisor
ltp = self.unpack_bytes(packet, start = 4, end = 8) / divisor
change = (ltp - prev_close)
pct_change = (change / prev_close) * 100.0
# Assume null for those things that will be given for only Full mode:
oi = None
oi_day_high = None
oi_day_low = None
depth = None
trade_ts = None
trade_tz = None
exchange_ts = None
exchange_tz = None
# If the tick is in Full mode:
if packet_size == 184:
oi = self.unpack_bytes(packet, start = 48, end = 52)
oi_day_high = self.unpack_bytes(packet, start = 52, end = 56)
oi_day_low = self.unpack_bytes(packet, start = 56, end = 60)
depth = {
"buy": [],
"sell": []
}
for i, p in enumerate(range(64, len(packet), 12)):
depth["sell" if i >= 5 else "buy"].append({
"quantity": self.unpack_bytes(packet, p, p + 4),
"price": self.unpack_bytes(packet, p + 4, p + 8) / divisor,
"orders": self.unpack_bytes(packet, p + 8, p + 10, byte_format = "H")
})
depth = ZerodhaKiteMarketDepth(**depth)
trade_ts, trade_tz = self.parse_exchange_ts(
timestamp = self.unpack_bytes(packet, start=44, end=48),
exchange = exchange_name,
to_timezone = date_time.TIMEZONE_UTC
)
exchange_ts, exchange_tz = self.parse_exchange_ts(
timestamp = self.unpack_bytes(packet, start=60, end=64),
exchange = exchange_name,
to_timezone = date_time.TIMEZONE_UTC
)
# Feed the model:
this_tick = ZerodhaKiteTick(
tickMode = mode,
exchange = exchange_name,
instrumentToken = instrument_token,
tradeable = tradeable,
prevClose = prev_close,
ltp = ltp,
qty = self.unpack_bytes(packet, start = 8, end = 12),
o = self.unpack_bytes(packet, start = 28, end = 32) / divisor,
h = self.unpack_bytes(packet, start = 32, end = 36) / divisor,
l = self.unpack_bytes(packet, start = 36, end = 40) / divisor,
c = ltp,
chg = change,
pChg = pct_change,
vwap = self.unpack_bytes(packet, start = 12, end = 16) / divisor,
totBuyQty = self.unpack_bytes(packet, start = 20, end = 24),
totSellQty = self.unpack_bytes(packet, start = 24, end = 28),
totVol = self.unpack_bytes(packet, start = 16, end = 20),
oi = oi,
oiDayHigh = oi_day_high,
oiDayLow = oi_day_low,
depth = depth,
tradeTs = trade_ts,
tradeTz = trade_tz,
exchgTs = exchange_ts,
exchgTz = exchange_tz
)
ticks.append(this_tick)
# Done here:
return ticks
async def _keep_listening_to_websocket(self) -> None:
"""
Go in an infinite loop and keep listening to messages (both, binary and text) from the websocket. Zerodha, as
per their documentation, will send tick updates exclusively in binary format. Some other kinds of messages can
also be sent by them in text format. These text messages include order updates, simple broker messages, and
error information.
DOCUMENTATION:
01. https://kite.trade/docs/connect/v3/websocket/
:return: None.
"""
# Create an action-map to call different functions when text messages come in:
text_message_action_map = {
"error": self._ws_on_error,
"order": self._ws_on_order_update,
"message": self._ws_on_message
}
# Keep listening while the websocket is alive:
while self.websocket_is_connected:
# Wait for a message from the server.
# Remember that this could be any of the above-mentioned messages:
message = None
try: message = await self._ws.recv()
except websockets.exceptions.ConnectionClosed as exception: pass
if message is None: continue
# Figure out what kind of message has come in:
is_binary = True if isinstance(message, bytes) else False
# Since some data has arrived, we trigger the general callback:
await self.invoke_callback(self._ws_on_data, self, message, is_binary)
# When the received data is just a set of bytes:
if is_binary:
# Binary tick updates:
if len(message) > 1:
ticks = self.decode_ticks(binary_data = message)
await self.invoke_callback(self._ws_on_ticks, self, ticks)
# Just a one-byte heartbeat to keep the connection alive.
# This can be safely ignored:
else: pass
# One of the text-format messages:
elif not is_binary:
message = json.from_string(message)
callback = text_message_action_map.get(message["type"])
await self.invoke_callback(callback, self, message)
# Invoke the disconnection callback:
await self.invoke_callback(self._ws_on_disconnect, self)
def set_websocket_callbacks(
self,
on_connect: Callable[["AsyncZerodhaKite"], Awaitable] = None,
on_no_connect: Callable[["AsyncZerodhaKite"], Awaitable] = None,
on_disconnect: Callable[["AsyncZerodhaKite"], Awaitable] = None,
on_reconnect: Callable[["AsyncZerodhaKite"], Awaitable] = None,
on_no_reconnect: Callable[["AsyncZerodhaKite"], Awaitable] = None,
on_data: Callable[["AsyncZerodhaKite", Union[bytes, str], bool], Awaitable] = None,
on_ticks: Callable[["AsyncZerodhaKite", List[ZerodhaKiteTick]], Awaitable] = None,
on_error: Callable[["AsyncZerodhaKite", dict], Awaitable] = None,
on_order_update: Callable[["AsyncZerodhaKite", dict], Awaitable] = None,
on_message: Callable[["AsyncZerodhaKite", dict], Awaitable] = None
) -> None:
"""
Just sets the callbacks for various websocket events. None of them are enforced as "mandatory".
:param on_connect: Triggered when the VERY FIRST connection happens.
:param on_no_connect: Triggered when ANY connection attempt fails.
:param on_disconnect: Triggered when an established connection is lost.
:param on_reconnect: Triggered when ANY re-connection attempt is successful.
:param on_no_reconnect: Triggered when NO MORE new re-connection attempts will be made.
:param on_data: Triggered when any kind of data is received (binary or text). If the data is of binary nature,
it will trigger the 'on_ticks' callback as well, and, if it is of text nature, it will trigger one of
'on_error', 'on_order_update', or 'on_message' callbacks. Be sure on how you will handle double-triggering.
:param on_ticks: Triggered when specifically tick data is received.
:param on_error: Triggered when an error message is received.
:param on_order_update: Triggered when an order update is received.
:param on_message: Triggered when any general message is received.
:return: None.
"""
# Accept the callbacks:
self._ws_on_connect = on_connect
self._ws_on_no_connect = on_no_connect
self._ws_on_disconnect = on_disconnect
self._ws_on_reconnect = on_reconnect
self._ws_on_no_reconnect = on_no_reconnect
self._ws_on_data = on_data
self._ws_on_ticks = on_ticks
self._ws_on_error = on_error
self._ws_on_order_update = on_order_update
self._ws_on_message = on_message
async def connect_websocket(
self,
ping_interval: float = 20.0,
ping_timeout: float = 20.0,
) -> None:
"""
To connect to the websocket for live-streaming of data/alerts from Zerodha. This method may also be invoked for
attempting to re-connect to the websocket. This method will invoke callbacks like 'on_connect', 'on_reconnect',
and 'on_no_connect' when applicable so do ensure that you set them up before calling this method. You may change
callbacks on the fly as long as their inputs match the specifications.
:return: None.
"""
# Ensure that no other async process does this concurrently,
# and do this only if the websocket is not already connected:
async with self._ws_conn_semaphore:
if not self.websocket_is_connected:
try:
# Try to connect to the websocket:
self._ws = await websockets.connect(
uri = f"{self.ROOT_WEBSOCKET_URL}?api_key={self._api_key}&access_token={self._access_token}",
ping_interval = ping_interval,
ping_timeout = ping_timeout
)
# Trigger the appropriate callback:
task = None
if self._ws_first_connect: task = self.invoke_callback(self._ws_on_connect, self)
else: task = self.invoke_callback(self._ws_on_reconnect)
if task: asyncio.create_task(task)
# Start listening for messages:
if self._ws_listener_task is None:
self._ws_listener_task = asyncio.create_task(self._keep_listening_to_websocket())
# Mark the flag that indicates first connection:
self._ws_first_connect = False
# In case of any connection failure:
except Exception as exception:
self._ws = None
await self.invoke_callback(self._ws_on_no_connect, self)
async def disconnect_websocket(self) -> None:
"""
Manually disconnect from the websocket.
:return: None.
"""
# Ensure that the connection is not being modified:
async with self._ws_conn_semaphore:
# Disconnect only if you are currently connected:
if self.websocket_is_connected:
# Kill the message-listening task:
if self._ws_listener_task:
self._ws_listener_task.cancel()
self._ws_listener_task = None
# Close the websocket connection:
await self._ws.close(code = 1000, reason = "See you later :)")
self._ws = None
# Since this is a manual close, prevent the
# auto-reconnection mechanism from connecting right back:
self._ws_made_conn_attempts = self._ws_max_conn_attempts + 1
# Invoke the on-disconnect callback:
await self.invoke_callback(self._ws_on_disconnect, self)
async def run_websocket(
self,
max_reconnect_attempts: int = 100,
backoff_seconds: float = 1.0,
backoff_multiplier: float = 1.0,
max_backoff_seconds: float = 5.0,
ping_interval: float = 5.0,
ping_timeout: float = 5.0,
custom_task: Callable[["AsyncZerodhaKite"], Awaitable] = None
) -> None:
"""
Start a websocket loop where the websocket is listening to all incoming messages and invoking associated
callbacks. If there is some other async task that you need to run simultaneously, you may pass it in through the
'custom_task' variable, but it may come at a cost to the performance. If you need a fully customised loop, use
the 'connect_websocket' method instead of this. That method doesn't have the auto-reconnect loop.
:param max_reconnect_attempts: The absolute maximum no. of connection attempts to make (whether successful or
not). After this the code will break out of the loop.
:param backoff_seconds: The no. of seconds to wait before trying to reconnect to the websocket.
:param backoff_multiplier: The amount by which to vary the backoff seconds parameter.
:param max_backoff_seconds: The absolute maximum seconds to wait for the next reconnection attempt.
:param ping_interval: The keep-alive interval.
:param ping_timeout: The timeout for the keep-alive signal.
:param custom_task: Any custom async task that you'd like to run in the background. Use this wisely, it can
cause a significant performance loss.
:return: None.
"""
# Accept the inputs:
self._ws_max_conn_attempts = max_reconnect_attempts
# Spawn the custom task:
asyncio.create_task(custom_task(self))
# Stay in the loop:
self._ws_made_conn_attempts = 0
while self._ws_made_conn_attempts <= self._ws_max_conn_attempts:
if not self.websocket_is_connected:
self._ws_made_conn_attempts += 1
await self.connect_websocket(
ping_interval = ping_interval,
ping_timeout = ping_timeout
)
await asyncio.sleep(min(backoff_seconds, max_backoff_seconds))
backoff_seconds *= backoff_multiplier
else: await asyncio.sleep(1.0)
# When the connection attempts are exhausted:
await self.invoke_callback(self._ws_on_no_reconnect, self)
async def subscribe_ticks(
self,
instrument_tokens: int | List[int]
):
"""
To subscribe to instruments for their tick-by-tick updates. This can be done at any time; even when already
connected and listening to ticks for other instruments. Note that Zerodha DOES NOT immediately give confirmation
of whether, or not, a particular instrument was subscribed-to successfully. Any intimation is given only in
text messages of the type "error".
DOCUMENTATION:
01. https://kite.trade/docs/connect/v3/websocket/
:param instrument_tokens: One or more instrument token whose live ticks are desired.
:return: None.
"""
# Ensure that the websocket is connected:
if self.websocket_is_connected:
# Ensure that no other async process is modifying
# the subscriptions at the same time:
async with self._ws_subscription_semaphore:
# Zerodha needs the instrument tokens as a list
# even if you just want to subscribe to one:
if not isinstance(instrument_tokens, list):
instrument_tokens = [instrument_tokens]
# Construct the message:
message_json = json.to_string(
{
"a": "subscribe",
"v": instrument_tokens
},
no_space = True
)
# Send the message:
await self._ws.send(message_json.encode(), text = True)
# Make a note of your subscriptions:
for token in instrument_tokens:
self._ws_subscribed_list[token] = self.MODE_QUOTE
async def unsubscribe_ticks(
self,
instrument_tokens: int | List[int]
):
"""
To unsubscribe from instruments for their tick-by-tick updates. This can be done at any time; even when already
connected and listening to ticks for any instruments. Note that Zerodha DOES NOT immediately give confirmation
of whether, or not, a particular instrument was unsubscribed-from successfully. Any intimation is given only in
text messages of the type "error".
DOCUMENTATION:
01. https://kite.trade/docs/connect/v3/websocket/
:param instrument_tokens: One or more instrument token whose live ticks are desired.
:return: None.
"""
# Ensure that the websocket is connected:
if self.websocket_is_connected:
# Ensure that no other async process is modifying
# the subscriptions at the same time:
async with self._ws_subscription_semaphore:
# Zerodha needs the instrument tokens as a list
# even if you just want to subscribe to one:
if not isinstance(instrument_tokens, list):
instrument_tokens = [instrument_tokens]
# Construct the message:
message_json = json.to_string(
{
"a": "unsubscribe",
"v": instrument_tokens
},
no_space = True
)
# Send the message:
await self._ws.send(message_json.encode(), text = True)
# Make a note of changes to your subscriptions:
for token in instrument_tokens:
self._ws_subscribed_list.pop(token, None)
async def set_mode_for_ticks(
self,
mode: Literal["ltp", "quote", "full"],
instrument_tokens: int | List[int]
):
"""
To set the mode for the instruments you have subscribed to.
DOCUMENTATION:
01. https://kite.trade/docs/connect/v3/websocket/
:param mode: The mode in which you want to receive data for the instruments.
:param instrument_tokens: One or more instrument token whose live ticks are desired.
:return: None.
"""
# Ensure that the websocket is connected:
if self.websocket_is_connected:
# Ensure that no other async process is modifying
# the subscriptions at the same time:
async with self._ws_subscription_semaphore:
# Zerodha needs the instrument tokens as a list
# even if you just want to subscribe to one:
if not isinstance(instrument_tokens, list):
instrument_tokens = [instrument_tokens]
# Filter out the instruments that you haven't subscribed to:
subscribed_tokens = self._ws_subscribed_list.keys()
instrument_tokens = [i for i in instrument_tokens if i in subscribed_tokens]
# Construct the message:
message_json = json.to_string(
{
"a": "mode",
"v": [mode, instrument_tokens]
},
no_space = True
)
# Send the message:
await self._ws.send(message_json.encode(), text = True)
# Make a note of changes to your subscriptions:
for token in instrument_tokens:
self._ws_subscribed_list[token] = mode
# *****************************************************************************************************************
# ***** ****
# *** MAIN PROGRAM ***
# ***** ****
# *****************************************************************************************************************
if __name__ == "__main__":
async def on_exception(
api_key: str,
user_profile: ZerodhaKiteUserProfile | None,
error_type: str,
error_json: dict,
api_response: ZerodhaKiteApiResponse
):
print("\n\n---\n\n")
print("EXCEPTION!")
print("USER:", user_profile)
print("API-KEY:", api_key)
print("ERROR-TYPE:", error_type)
print("ERROR:", error_json)
print("URL:", f"[{api_response.method}] {api_response.url}")
async def on_ticks(client: AsyncZerodhaKite, ticks):
for tick in ticks:
print("TICK:", tick.model_dump())
# Read the credentials:
creds = json.from_file(r"../../../../creds/zerodha/api.json")
# Create the client:
my_kite = AsyncZerodhaKite(
api_key = creds["apiKey"],
api_secret = creds["apiSecret"],
on_token_exception = on_exception,
on_user_exception = on_exception,
on_order_exception = on_exception,
on_input_exception = on_exception,
on_margin_exception = on_exception,
on_holding_exception = on_exception,
on_network_exception = on_exception,
on_data_exception = on_exception,
on_general_exception = on_exception,
on_undocumented_exception = on_exception,
)
# async def on_connect(client: AsyncZerodhaKite):
# print("ON CONNECT!")
# while not client.websocket_is_connected: await asyncio.sleep(0.1)
# inst_tok = [408065, 884737]
# await client.subscribe_ticks(inst_tok)
# await client.set_mode_for_ticks(client.MODE_FULL, inst_tok)
#
# async def no_connect(client: AsyncZerodhaKite):
# print("NOOOOO CONNECT :(")
#
# async def no_reconnect_bhopli(client: AsyncZerodhaKite):
# print("NOOOOO REEE-CONNECT")
#
# async def reconnect(client: AsyncZerodhaKite):
# print("RE-CONNECT")
#
# async def on_disconnect(client: AsyncZerodhaKite):
# print("DISCONNECTED!")
#
# async def custom_task(client: AsyncZerodhaKite):
# for _ in range(60):
# await asyncio.sleep(1.0)
# print(f"WS LATENCY ({_}):", my_kite.websocket_latency)
# # await asyncio.sleep(60.0)
# await client.disconnect_websocket()
async def main():
# Login flow:
print("LOGIN URL:", my_kite.login_url)
my_kite.set_request_token(input("Request Token: "))
auth_tokens = await my_kite.generate_session()
print(auth_tokens)
# In case you had logged-in and already have the access token:
my_kite.set_access_token(access_token = creds["accessToken"])
# # Logout:
# api_response = await my_kite.logout()
# print(api_response.to_markdown())
# # User info:
# user_profile = await my_kite.get_user_profile()
# if user_profile: print(json.to_string(user_profile.model_dump()))
# user_funds = await my_kite.get_user_funds()
# if user_funds: print(json.to_string(user_funds.model_dump()))
# # Portfolio info:
# user_holdings = await my_kite.get_user_holdings()
# # if user_holdings: print(json.to_string(user_holdings.model_dump()))
# user_positions = await my_kite.get_user_positions()
# # if user_positions: print(json.to_string(user_positions.model_dump()))
# Instruments:
# instruments_list = await my_kite.list_instruments()
# print(instruments_list)
# print("COUNT:", len(instruments_list))
# my_kite.set_websocket_callbacks(
# on_connect = on_connect,
# on_reconnect = reconnect,
# on_no_connect = no_connect,
# on_ticks = on_ticks,
# on_no_reconnect = no_reconnect_bhopli,
# on_disconnect = on_disconnect
# )
# await my_kite.run_websocket(
# max_reconnect_attempts = 50,
# backoff_seconds = 1.0,
# backoff_multiplier = 1.1,
# max_backoff_seconds = 5.0,
# custom_task = custom_task,
# )
asyncio.run(main())