diff --git a/api/blueprints/finstitutions/trading/oauth/callback.py b/api/blueprints/finstitutions/trading/oauth/callback.py index 3a2c334..623d92d 100644 --- a/api/blueprints/finstitutions/trading/oauth/callback.py +++ b/api/blueprints/finstitutions/trading/oauth/callback.py @@ -112,7 +112,7 @@ def init(blueprint_setup_state): # --------------------------------------------------------------------------------------------------------------------- -@trading_oauth_callback_bp.route("oauth/callback/", methods = ["POST", "GET"]) +@trading_oauth_callback_bp.route("/callback/", methods = ["POST", "GET"]) @set_api_version(api_version = "1.0.0") @read_input(sanitize_headers = False, sanitize_data = False) @log_request_to_mongo( diff --git a/api/blueprints/finstitutions/trading/oauth/request.py b/api/blueprints/finstitutions/trading/oauth/request.py index ad4be42..d4f4b73 100644 --- a/api/blueprints/finstitutions/trading/oauth/request.py +++ b/api/blueprints/finstitutions/trading/oauth/request.py @@ -64,7 +64,7 @@ from shared import constants # Data Models: from models.core.auth_token import CoreAuthTokenModel -from models.api.finstitutions.trading.auth import ( +from models.api.finstitutions.trading.auth.oauth import ( TradingAuthRequestHeaders, TradingAuthRequestData ) @@ -112,7 +112,7 @@ def init(blueprint_setup_state): # --------------------------------------------------------------------------------------------------------------------- -@trading_oauth_request_bp.route("/oauth", methods = ["GET"]) +@trading_oauth_request_bp.route("", methods = ["GET"]) @set_api_version(api_version = "1.0.0") @read_input(sanitize_headers = False, sanitize_data = False) @get_session_info(key = "X-Session-Token", session_coro = "get_session") diff --git a/api/blueprints/finstitutions/trading/symbols/__init__.py b/api/blueprints/finstitutions/trading/symbols/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/blueprints/finstitutions/trading/symbols/list.py b/api/blueprints/finstitutions/trading/symbols/list.py new file mode 100644 index 0000000..a083b1e --- /dev/null +++ b/api/blueprints/finstitutions/trading/symbols/list.py @@ -0,0 +1,214 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Monday, 23rd Dec., 2024 + + OBJECTIVE: + + To receive requests to list tradeable symbols for various stockbrokers like Zerodha. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + + NOTES: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# For using Quart: +from quart import Blueprint, current_app, request + +# My utils: +from utils_v2.string import json +from utils_v2.api.codes import StatusCodes, HttpCodes +from utils_v2.api.response import ResponseModel +from utils_v2.api.async_quart import ( + set_api_version, + read_input, + get_session_info, + log_request_to_mongo, + log_chain_to_mongo, + should_not_be_under_maintenance, + only_whitelisted_ips, + limit_rate, + validate_input, + handle_cancelled_request +) + +# Common: +from shared import constants + +# Data Models: +from models.core.auth_token import CoreAuthTokenModel +from models.api.finstitutions.trading.symbols.list import ( + TradingSymbolListRequestHeaders, + TradingSymbolListRequestData, + TradingSymbolListBrokerResponse +) + +# For asynchronous activities: +import asyncio + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# Related to Quart: +trading_symbols_list_bp = Blueprint("trdng_sym_list", __name__) + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +@trading_symbols_list_bp.record_once +def init(blueprint_setup_state): + + # This gets called when the blueprint is registered. + # Consider this to be a one-time setup for the whole blueprint: + pass + + +# --------------------------------------------------------------------------------------------------------------------- + + +@trading_symbols_list_bp.route("/list", methods = ["GET"]) +@set_api_version(api_version = "1.0.0") +@read_input(sanitize_headers = False, sanitize_data = False) +@get_session_info(key = "X-Session-Token", session_coro = "get_session") +@log_request_to_mongo( + attr_name = "logs_mongo", + project = constants.PROJECT_NAME, + log_type = constants.MODULE_NAME, + operation = "trdngSymListReqApi", + log_input = True, + log_output = True, + sensitive_keys = ["sessionToken", "X-Session-Token", "tokenKey", "tokenKeys"] +) +@log_chain_to_mongo(attr_name = "logs_mongo") +@should_not_be_under_maintenance(attr_name = "is_under_maintenance") +@validate_input( + header_validator = lambda x: TradingSymbolListRequestHeaders(**x).model_dump(), + data_validator = lambda x: TradingSymbolListRequestData(**x) +) +@handle_cancelled_request() +async def request_oauth_authorization_url( + inbound_headers: dict | TradingSymbolListRequestHeaders = None, + inbound_data: dict | TradingSymbolListRequestData = None, + inbound_files: dict = None, + **kwargs +): + + """ + Use this when you want a list of all tradeable symbols from a particular broker. + :param inbound_headers: auto-extracted by the decorators. + :param inbound_data: auto-extracted by the decorators. + :param inbound_files: auto-extracted by the decorators. + :param kwargs: Any number of extra inputs supplied by the decorators. + :return: A standard response structure. + """ + + # ┏┓ + # ┃┃┏┓┏┓┏┓┏┓┏┓┏┏┓┏┏ + # ┣┛┛ ┗ ┣┛┛ ┗┛┗┗ ┛┛ + # ┛ + + # If the session token is invalid/expired: + if kwargs.get("session_info") is None: + return ResponseModel( + status_code = StatusCodes.FAILED, + http_code = HttpCodes.UNAUTHORIZED + ) + + # Get the auth-token: + auth_token = await current_app.trading_controller.get_token_from_key( + mongo_data_conn = current_app.data_mongo, + token_key = inbound_data.tokenKey + ) + if auth_token is None: return ResponseModel( + status_code = StatusCodes.FAILED, + http_code = HttpCodes.UNAUTHORIZED, + message = f"No such token key." + ) + + # Start by assuming failure: + symbol_list = TradingSymbolListBrokerResponse() + symbol_list.message = f"Invalid/unimplemented client." + + # ┏┓ ┏┓ ┓┓ ┓┏┓• + # ┣ ┏┓┏┓ ┏┛┏┓┏┓┏┓┏┫┣┓┏┓ ┃┫ ┓╋┏┓ + # ┻ ┗┛┛ ┗┛┗ ┛ ┗┛┗┻┛┗┗┻ ┛┗┛┗┗┗ + + if auth_token.client == "zerodhaKite": + + symbol_list = await current_app.zerodha_kite_controller.list_symbols( + mongo_data_conn = current_app.data_mongo, + auth_token = auth_token, + inbound_data = inbound_data + ) + + # ┳┓ + # ┣┫┏┓┏┏┓┏┓┏┓┏┏┓ + # ┛┗┗ ┛┣┛┗┛┛┗┛┗ + # ┛ + + # Done here: + return ResponseModel( + status_code = StatusCodes.OK if symbol_list.success else StatusCodes.FAILED, + http_code = HttpCodes.SUCCESS if symbol_list.success else HttpCodes.INTERNAL_SERVER_ERROR, + data = [s.model_dump() for s in symbol_list.data] if symbol_list.data is not None else symbol_list.data, + message = symbol_list.message + ) + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/api/blueprints/sms/send_v2.py b/api/blueprints/sms/send_v2.py index ff61e67..a8f885e 100644 --- a/api/blueprints/sms/send_v2.py +++ b/api/blueprints/sms/send_v2.py @@ -223,7 +223,7 @@ async def send_sms_messages_api( if auth_token is None: return ResponseModel( status_code = StatusCodes.FAILED, http_code = HttpCodes.UNAUTHORIZED, - message = f"no such token key" + message = f"No such token key." ) # ┏┓ ┓ ┏┳┓┓ ┏┓┳┳┓┏┓ diff --git a/api/main.py b/api/main.py index 74c616b..471a52e 100644 --- a/api/main.py +++ b/api/main.py @@ -122,6 +122,7 @@ from api.blueprints.finstitutions.payments.tags import pg_tags_update_bp # Finstitutions / Trading Blueprints: from api.blueprints.finstitutions.trading.oauth.request import trading_oauth_request_bp from api.blueprints.finstitutions.trading.oauth.callback import trading_oauth_callback_bp +from api.blueprints.finstitutions.trading.symbols.list import trading_symbols_list_bp # AI Blueprints: from api.blueprints.ai.llm.invoke import llm_invoke_bp @@ -188,8 +189,9 @@ app.register_blueprint(pg_get_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/pa app.register_blueprint(pg_tags_update_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/payments") # Finstitutions / Trading Blueprints: -app.register_blueprint(trading_oauth_request_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/trading") -app.register_blueprint(trading_oauth_callback_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/trading") +app.register_blueprint(trading_oauth_request_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/trading/oauth") +app.register_blueprint(trading_oauth_callback_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/trading/oauth") +app.register_blueprint(trading_symbols_list_bp, url_prefix = f"/{MODULE_BASE}/finstitutions/trading/symbols") # AI Blueprints: app.register_blueprint(llm_invoke_bp, url_prefix = f"/{MODULE_BASE}/ai") diff --git a/controllers_v2/finstitutions/trading/all_trading.py b/controllers_v2/finstitutions/trading/all_trading.py index 78a6d47..0d88927 100644 --- a/controllers_v2/finstitutions/trading/all_trading.py +++ b/controllers_v2/finstitutions/trading/all_trading.py @@ -36,6 +36,7 @@ sys.path.append(".") sys.path.append("..") # My async utils: +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 @@ -44,6 +45,7 @@ from controllers_v2.finstitutions.trading.base import TradingController # Models: from models.core.auth_token import CoreAuthTokenModel +from models.api.finstitutions.trading.symbols.list import TradingSymbolListRequestData, TradingSymbolListBrokerResponse # To work with datatypes: from typing import List, Any @@ -131,7 +133,58 @@ class AllTradingController(TradingController): # ┣┫┓┏╋┣┓ # ┛┗┗┻┗┛┗ - pass + @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. + """ + + raise NotImplementedError + + async def handle_authorization_callback( + self, + sql_conn: AsyncMySQL, + mongo_data_conn: AsyncMongo, + inbound_data: dict + ) -> bool: + """ + When the end user interacts with their broker's APIs, the broker's servers would usually issue a callback. + We've seen this in the case of Zerodha Kite and ICICI Breeze. Use this method to handle the callback loop to + complete the authorization. + :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. + """ + + raise NotImplementedError + + # ┏┳┓ ┓• ┏┓ ┓ ┓ + # ┃ ┏┓┏┓┏┫┓┏┓┏┓ ┗┓┓┏┏┳┓┣┓┏┓┃┏ + # ┻ ┛ ┗┻┗┻┗┛┗┗┫ ┗┛┗┫┛┗┗┗┛┗┛┗┛ + # ┛ ┛ + + 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 a broker. + :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. + """ + + raise NotImplementedError # ***************************************************************************************************************** diff --git a/controllers_v2/finstitutions/trading/base.py b/controllers_v2/finstitutions/trading/base.py index 7f9323d..0f674ca 100644 --- a/controllers_v2/finstitutions/trading/base.py +++ b/controllers_v2/finstitutions/trading/base.py @@ -36,6 +36,7 @@ sys.path.append(".") sys.path.append("..") # My async utils: +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 @@ -44,6 +45,7 @@ from controllers_v2.core.auth_token import CoreAuthTokenController # Models: from models.core.auth_token import CoreAuthTokenModel +from models.api.finstitutions.trading.symbols.list import TradingSymbolListRequestData, TradingSymbolListBrokerResponse # To work with datatypes: from typing import List, Any @@ -149,7 +151,62 @@ class TradingController(CoreAuthTokenController, ABC): # ┣┫┓┏╋┣┓ # ┛┗┗┻┗┛┗ - pass + @staticmethod + @abstractmethod + 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. + """ + + pass + + @abstractmethod + async def handle_authorization_callback( + self, + sql_conn: AsyncMySQL, + mongo_data_conn: AsyncMongo, + inbound_data: dict + ) -> bool: + + """ + When the end user interacts with their broker's APIs, the broker's servers would usually issue a callback. + We've seen this in the case of Zerodha Kite and ICICI Breeze. Use this method to handle the callback loop to + complete the authorization. + :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. + """ + + pass + + # ┏┳┓ ┓• ┏┓ ┓ ┓ + # ┃ ┏┓┏┓┏┫┓┏┓┏┓ ┗┓┓┏┏┳┓┣┓┏┓┃┏ + # ┻ ┛ ┗┻┗┻┗┛┗┗┫ ┗┛┗┫┛┗┗┗┛┗┛┗┛ + # ┛ ┛ + + @abstractmethod + 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 a broker. + :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. + """ + + pass # ***************************************************************************************************************** diff --git a/controllers_v2/finstitutions/trading/zerodha_kite.py b/controllers_v2/finstitutions/trading/zerodha_kite.py index 8697022..a316eee 100644 --- a/controllers_v2/finstitutions/trading/zerodha_kite.py +++ b/controllers_v2/finstitutions/trading/zerodha_kite.py @@ -47,6 +47,11 @@ 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 @@ -100,6 +105,12 @@ from kiteconnect import KiteConnect class ZerodhaKiteTradingController(TradingController): + # ┏┓┓ ┓┏ + # ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏ + # ┗┛┗┗┻┛┛ ┗┛┗┻┛ ┛ + + CLIENT_NAME = "zerodhaKite" + # ┏┓ # ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓ # ┗┛┗┛┛┗┛┗┛ ┗┻┗┗┗┛┛ @@ -125,7 +136,7 @@ class ZerodhaKiteTradingController(TradingController): """ # Declare the client: - this_client = "zerodhaKite" + this_client = self.CLIENT_NAME # Prepare base filter: this_filter = {"client": this_client} @@ -179,7 +190,7 @@ class ZerodhaKiteTradingController(TradingController): :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: The model that hold the access tokens, or None if something failed. + :return: True if the callback loop was completed successfully, else False.. """ # Start by assuming failure: @@ -229,6 +240,55 @@ class ZerodhaKiteTradingController(TradingController): # 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 + # ***************************************************************************************************************** # ***** **** diff --git a/models/api/finstitutions/trading/auth/__init__.py b/models/api/finstitutions/trading/auth/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/models/api/finstitutions/trading/auth.py b/models/api/finstitutions/trading/auth/oauth.py similarity index 100% rename from models/api/finstitutions/trading/auth.py rename to models/api/finstitutions/trading/auth/oauth.py diff --git a/models/api/finstitutions/trading/symbols/__init__.py b/models/api/finstitutions/trading/symbols/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/models/api/finstitutions/trading/symbols/list.py b/models/api/finstitutions/trading/symbols/list.py new file mode 100644 index 0000000..530b02a --- /dev/null +++ b/models/api/finstitutions/trading/symbols/list.py @@ -0,0 +1,324 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Monday, 23rd Dec., 2024. + + OBJECTIVE: + + To provide a structure to receive symbol/instrument listing requests for stock trading brokers like Zerodha. + + REFERENCES: + + N/A + + DOWNLOADS: + + N/A + +""" + + +# ***************************************************************************************************************** +# ***** **** +# *** IMPORT *** +# ***** **** +# ***************************************************************************************************************** + + +# To make sibling directories accessible for imports: +import sys +sys.path.append(".") +sys.path.append("..") + +# For making data behaviour_models: +from pydantic import BaseModel, Field, field_validator, PastDatetime, model_validator, AwareDatetime +from typing import Optional, Literal, Union, List + +# My utils: +from utils_v2.string import regex +from utils_v2.date_time import date_time + +# To work with date and time: +import datetime + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# RegEx Patterns: +REGEX_SESSION_TOKEN = r"^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +class TradingSymbol(BaseModel): + + exchange: Literal["NSE", "NFO", "BSE", "BFO", "MCX", "CDS", "BCD"] = Field( + description = "the exchange on which this symbol is traded", + frozen = True + ) + + exchangeToken: str | int = Field( + description = "the code by which the exchange identifies this instrument", + frozen = True + ) + + broker: Literal["zerodhaKite", "iciciBreeze"] = Field( + description = "the broker that gave you the details of this instrument", + frozen = True + ) + + brokerToken: str | int = Field( + description = "the code by which the broker identifies this instrument", + frozen = True + ) + + name: str = Field( + description = "the name of the co./asset", + frozen = True + ) + + symbol: str = Field( + description = "tha trading symbol pf the co./asset", + frozen = True + ) + + tickSize: float = Field( + description = "the minimum step size in the change of price of the instrument", + frozen = True + ) + + ltp: float = Field( + description = "the last price of this instrument at the time of requesting the symbol list", + frozen = True + ) + + segment: str = Field( + description = "the segment which this asset represents", + frozen = True + ) + + type: str = Field( + description = "the type of the instrument in the segment", + frozen = True + ) + + lotSize: int = Field( + description = "the minimum tradeable qty of this instrument", + frozen = True + ) + + strike: int | float | None = Field( + description = "the strike price of the instrument if it is a derivative", + default = None, + frozen = True + ) + + expiryTs: AwareDatetime | None = Field( + description = "the expiry (utc) of this instrument if it is a derivative", + default = None, + frozen = True + ) + + expiryTz: str = Field( + description = "the timezone in which the expiry timestamp my be interpreted; should be compatible with pytz", + frozen = True + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + # ┏┓ ┏┓ + # ┃ ┓┏┏╋┏┓┏┳┓ ┣ ┓┏┏┓┏┏ + # ┗┛┗┻┛┗┗┛┛┗┗ ┻ ┗┻┛┗┗┛ + + @staticmethod + def from_zerodha_kite(instrument: dict): + return TradingSymbol( + exchange = instrument["exchange"], + exchangeToken = instrument["exchange_token"], + broker = "zerodhaKite", + brokerToken = instrument["instrument_token"], + name = instrument["name"], + symbol = instrument["tradingsymbol"], + tickSize = instrument["tick_size"], + ltp = instrument["last_price"], + segment = instrument["segment"], + type = instrument["instrument_type"], + lotSize = instrument["lot_size"], + strike = instrument["strike"], + expiryTs = date_time.to_timezone( + datetime_object = datetime.datetime.combine( + instrument["expiry"], + datetime.time(hour = 0, minute = 0, second = 0) + ), + timezone = date_time.TIMEZONE_UTC + ), + expiryTz = "Asia/Kolkata" + ) + + # ┓┏ ┓• ┓ • + # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ + # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ + + @field_validator("expiryTs", mode = "before") + def parse_date_time(cls, value): + + if not value: value = None + + if isinstance(value, str): + value = value.strip() + value = date_time.parse_date_time( + input_value = value, + timezone = date_time.TIMEZONE_UTC + ) + + if isinstance(value, datetime.datetime): + value = date_time.to_timezone( + value, + timezone = date_time.TIMEZONE_UTC + ) + + return value + + +# --------------------------------------------------------------------------------------------------------------------- + + +class TradingSymbolListBrokerResponse(BaseModel): + + success: bool = Field( + description = "whether, or not, the symbol list request was successful", + default = False, + frozen = False + ) + + message: str = Field( + description = "a brief message to help debug in failed cases", + default = "", + frozen = False + ) + + data: List[TradingSymbol] | None = Field( + description = "the actual response from the broker with his list of tradeable symbols", + default = None, + frozen = False + ) + + exception: Exception | None = Field( + description = "if something goes wrong, the exception will be held here", + default = None, + frozen = False + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + arbitrary_types_allowed = True + + +# --------------------------------------------------------------------------------------------------------------------- + + +class TradingSymbolListRequestHeaders(BaseModel): + + sessionToken: str = Field( + description = "the session token of the user who is requesting the service", + pattern = REGEX_SESSION_TOKEN, + frozen = True, + alias = "X-Session-Token" + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "allow" + + def model_dump(self, *args, **kwargs): + return super().model_dump(*args, by_alias = True, **kwargs) + + +# --------------------------------------------------------------------------------------------------------------------- + + +class TradingSymbolListRequestData(BaseModel): + + tokenKey: str = Field( + description = "the token identifier(s) that tell you which auth-tokens were used for fetching those messages", + frozen = True + ) + + exchanges: str | None | List[str | None] = Field( + description = ( + "the exchange whose tradeable symbols are of interest to us; " + "wherever null is not applicable, a default value will be taken" + ), + default = None, + frozen = True, + examples = ["NSE", "NFO", "BSE", "BFO", "MCX", "CDS", "BCD"] + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + # ┓┏ ┓• ┓ • + # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ + # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ + + @field_validator("exchanges", mode = "before") + def ensure_list(cls, value): + if not isinstance(value, list): value = [value] + return value + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/utils_v2/trading/zerodha_kite/models/auth_tokens.py b/utils_v2/trading/zerodha_kite/models/auth_tokens.py index 03229fa..a22ae1a 100644 --- a/utils_v2/trading/zerodha_kite/models/auth_tokens.py +++ b/utils_v2/trading/zerodha_kite/models/auth_tokens.py @@ -159,6 +159,7 @@ class ZerodhaKiteAuthTokens(BaseModel): class Config: extra = "ignore" + populate_by_name = True # ┓┏ ┓• ┓ • # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓