""" AUTHOR: Khushal P Soonderji DATE: Saturday, 21st Dec., 2024 OBJECTIVE: To handle all trading related behaviour for Zerodha's Kite platform. REFERENCES: N/A DOWNLOADS: N/A """ # ***************************************************************************************************************** # ***** **** # *** IMPORT *** # ***** **** # ***************************************************************************************************************** # To make sibling directories accessible for imports: import sys sys.path.append(".") sys.path.append("..") # My async utils: from utils_v2.string import json from utils_v2.database.async_mysql_v2 import AsyncMySQL from utils_v2.database.async_mongo_v2 import AsyncMongo from utils_v2.cache.async_redis_cache_v2 import AsyncRedisCache # Controllers: from controllers_v2.finstitutions.trading.base import TradingController # Models: from models.core.auth_token import CoreAuthTokenModel from utils_v2.trading.zerodha_kite.models.auth_tokens import ZerodhaKiteAuthTokens from models.api.finstitutions.trading.symbols.list import ( TradingSymbolListRequestData, TradingSymbolListBrokerResponse, TradingSymbol ) # To work with MongoDB: from bson.objectid import ObjectId # To work with datatypes: from typing import List, Any # To make HTTP requests: import httpx # To work with Zerodha's Kite platform: from kiteconnect import KiteConnect # ***************************************************************************************************************** # ***** **** # *** MACROS / ONE-TIME INIT *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** VARIABLES *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** FUNCTIONS *** # ***** **** # ***************************************************************************************************************** # --- Nothing Yet # ***************************************************************************************************************** # ***** **** # *** CLASSES *** # ***** **** # ***************************************************************************************************************** class ZerodhaKiteTradingController(TradingController): # ┏┓┓ ┓┏ # ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏ # ┗┛┗┗┻┛┛ ┗┛┗┻┛ ┛ CLIENT_NAME = "zerodhaKite" # ┏┓ # ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓ # ┗┛┗┛┛┗┛┗┛ ┗┻┗┗┗┛┛ def __init__( self, cache: AsyncRedisCache = None, http_client: httpx.AsyncClient = None, alert_url: str = None, debug: bool = True, debug_prefix: str = "Zerodha kite (C) | ", debug_only_errors: bool = True ): """ This is the foundational controller for Zerodha's Kite platform. :param cache: The object to use for caching results from database calls. :param http_client: The HTTP client :param debug: Whether, or not, you would like to print debugging messages: :param debug_prefix: The prefix to print with the debugging messages. :param debug_only_errors: Whether you would like to print only error messages or all messages. :return: None. """ # Declare the client: this_client = self.CLIENT_NAME # Prepare base filter: this_filter = {"client": this_client} # Invoke the parent's constructor: super().__init__( cache = cache, alert_url = alert_url, http_client = http_client, base_filter = this_filter, debug = debug, debug_prefix = debug_prefix, debug_only_errors = debug_only_errors ) # Init a variable in a parent: self._client = this_client # ┏┓ ┓ # ┣┫┓┏╋┣┓ # ┛┗┗┻┗┛┗ @staticmethod async def get_authorization_url( **kwargs ) -> str: """ To generate an authorization URL for this broker. :param kwargs: Any no. of things needed by your broker to generate the URL. :return: The authorization URL. """ return f"https://kite.zerodha.com/connect/login?api_key={kwargs['api_key']}" async def handle_authorization_callback( self, sql_conn: AsyncMySQL, mongo_data_conn: AsyncMongo, inbound_data: dict ) -> bool: """ When the end user interacts with Zerodha's APIs, Zerodha's servers issue a callback like this: http://127.0.0.1:5999/auth/callback?action=login&type=login&status=success&request_token=the-request-token We must use the request token to get the access token. The access token is the thing that we must hold onto for executing actual actions like subscribing to live market feed, placing trades, etc. NOTE: Please ensure that you set the 'Redirect URL' such that is passes back Kite's 'api_key' back through the callback URL. This can be one by setting the value manually as a query param on the app's configuration page. E.g.: http://127.0.0.1:5999/auth/callback?api_key=user_api_key :param sql_conn: The database connection to use to perform this activity. :param mongo_data_conn: The database connection to use to perform this activity. :param inbound_data: The data that came in from the broker. This could be in the JSON body, query params, etc. :return: True if the callback loop was completed successfully, else False.. """ # Start by assuming failure: success = False zerodha_auth_token = None # Get the token from the database: auth_token = await self.get_token_from_filter( mongo_data_conn = mongo_data_conn, filter_json = mongo_data_conn.dict_to_dot_notation({ "auth": { "apiKey": inbound_data.get( "api_key", "Hint: Put the user's app's key in the query params of the 'Redirect URL'" ) } }) ) # If not such auth token exists: if not auth_token: return success # Get the final access tokens set from Zerodha Kite: kite = KiteConnect(api_key = auth_token.auth["apiKey"]) session_data = kite.generate_session( request_token = inbound_data["request_token"], api_secret = auth_token.auth["apiSecret"] ) zerodha_auth_token = ZerodhaKiteAuthTokens(**session_data) # Prepare the inputs to save to the database: auth_url = await self.get_authorization_url(api_key = auth_token.auth["apiKey"]) auth_token.token = zerodha_auth_token.model_dump() # Save the additional auth info to the database: success = await self.set_token( sql_conn = sql_conn, mongo_data_conn = mongo_data_conn, token_key = auth_token.key, auth_token = auth_token, token_notes = { "apiKey": auth_token.auth["apiKey"], "authUrl": auth_url } ) # Done here: return success # ┏┳┓ ┓• ┏┓ ┓ ┓ # ┃ ┏┓┏┓┏┫┓┏┓┏┓ ┗┓┓┏┏┳┓┣┓┏┓┃┏ # ┻ ┛ ┗┻┗┻┗┛┗┗┫ ┗┛┗┫┛┗┗┗┛┗┛┗┛ # ┛ ┛ async def list_symbols( self, mongo_data_conn: AsyncMongo, auth_token: CoreAuthTokenModel, inbound_data: TradingSymbolListRequestData ) -> TradingSymbolListBrokerResponse: """ To get the list of tradeable symbols offered by Zerodha. :param mongo_data_conn: The database connection to use to perform this activity. :param auth_token: The token that has to be used to fetch the data. :param inbound_data: The data that came in with the APi call. :return: The structured response form the broker. """ # Start by assuming failure: symbol_list = TradingSymbolListBrokerResponse() try: # Fit the token into the model: zerodha_token = ZerodhaKiteAuthTokens(**auth_token.token) # Now create an instance of the Kite: kite = KiteConnect(api_key = auth_token.auth["apiKey"]) kite.set_access_token(zerodha_token.accessToken) # Now we retrieve the list of symbols for every kind of exchange: symbol_list.data = [] for exchange in inbound_data.exchanges: symbols_subset = kite.instruments(exchange = exchange) symbols_subset = [TradingSymbol.from_zerodha_kite(s) for s in symbols_subset[:10]] symbol_list.data += symbols_subset symbol_list.success = True symbol_list.message = "Symbol list retrieved successfully." # In case something goes wrong: except Exception as exception: symbol_list.exception = exception symbol_list.message = str(exception) # Done here: return symbol_list # ***************************************************************************************************************** # ***** **** # *** MAIN PROGRAM *** # ***** **** # ***************************************************************************************************************** if __name__ == "__main__": pass