diff --git a/wsocket/__init__.py b/background/__init__.py similarity index 100% rename from wsocket/__init__.py rename to background/__init__.py diff --git a/wsocket/finstitutions/__init__.py b/background/finstitutions/__init__.py similarity index 100% rename from wsocket/finstitutions/__init__.py rename to background/finstitutions/__init__.py diff --git a/wsocket/finstitutions/trading/__init__.py b/background/finstitutions/trading/__init__.py similarity index 100% rename from wsocket/finstitutions/trading/__init__.py rename to background/finstitutions/trading/__init__.py diff --git a/wsocket/main_bkp.py b/background/finstitutions/trading/json.py similarity index 51% rename from wsocket/main_bkp.py rename to background/finstitutions/trading/json.py index 428162a..ba71942 100644 --- a/wsocket/main_bkp.py +++ b/background/finstitutions/trading/json.py @@ -6,22 +6,23 @@ DATE: - monday, 23rd Dec., 2024 + Tuesday, 24th Aug. 2024 OBJECTIVE: - To provide a SocketIO app for socket-base communication with the front-end. + To provide an easy way to work with '.json' data and files. REFERENCES: - 01. YouTube: https://www.youtube.com/watch?v=H1eLJMC5oTg&t=3s + 1) https://www.w3schools.com/python/python_json.asp DOWNLOADS: N/A """ -import datetime + + # ***************************************************************************************************************** # ***** **** # *** IMPORT *** @@ -36,26 +37,12 @@ sys.path.append("..") # System-level activities: import io -import os -# my utils: -from utils_v2.string import json +# To work with the JSON standard: +import json -# To work with SocketIO: -import socketio -import eventlet - -# For asynchronous activities: -import asyncio - -# for debugging: -from icecream import IceCreamDebugger - -# To work with date and time: -import time - -# To work with Zerodha's Kite platform: -from kiteconnect import KiteConnect, KiteTicker +# To work with files: +from utils_v2.system import files # ***************************************************************************************************************** @@ -65,23 +52,7 @@ from kiteconnect import KiteConnect, KiteTicker # ***************************************************************************************************************** -INSTRUMENT_MAP = { - 256265: "NIFTY 50", - 260617: "NIFTY 100", - 259849: "NIFTY IT", - 341249: "HDFCBANK", - 738561: "RELIANCE", - 408065: "INFY", - 2953217: "TCS", - 356865: "HINDUNILVR", - 1270529: "ICICIBANK", - 492033: "KOTAKBANK", - 110630919: "GOLD25JAN75800CE", - 110050823: "SILVER25FEB76000CE", - 10670594: "NIFTY24DEC23650PE", - 17167874: "BANKNIFTY24DEC45000PE", -} -INSTRUMENT_TOKENS = list(INSTRUMENT_MAP.keys()) +# --- Nothing Yet # ***************************************************************************************************************** @@ -91,12 +62,7 @@ INSTRUMENT_TOKENS = list(INSTRUMENT_MAP.keys()) # ***************************************************************************************************************** -# The SocketIo server: -sio = socketio.Server(cors_allowed_origins = "*") -app = socketio.WSGIApp(sio) - -# Debugging: -printer = IceCreamDebugger(prefix = "SocketIO | ", includeContext = True) +# --- Nothing Yet # ***************************************************************************************************************** @@ -106,83 +72,121 @@ printer = IceCreamDebugger(prefix = "SocketIO | ", includeContext = True) # ***************************************************************************************************************** -@sio.event -def connect(sid, environ): - printer(sid) +def from_string(json_data): + + """ + Decodes a JSON string to a pythonic variable like a dict. + :param json_data: The JSON string to decode. + :return: The decoded pythonic variable. + """ + + python_data = json.loads(json_data) + return python_data # --------------------------------------------------------------------------------------------------------------------- -@sio.event -def disconnect(sid): - printer(sid) - - -# --------------------------------------------------------------------------------------------------------------------- - - -def on_ticks(ws, ticks): - - try: - - # print(json.to_string(ticks[0], default=str)) - printer(len(ticks)) - now = datetime.datetime.now() - for t in ticks: - t["last_trade_time"] = t.get("last_trade_time", now).strftime("%Y-%m-%d %H:%M:%S") - t["exchange_timestamp"] = t.get("exchange_timestamp", now).strftime("%Y-%m-%d %H:%M:%S") - sio.emit("ticks", ticks) - sio.emit("ticks", {"name": "Bhopli"}) - sio.emit("debug", {"name": "Debugger Bhopli"}) - - except Exception as exception: - printer(exception) - - -# --------------------------------------------------------------------------------------------------------------------- - - -def on_connect(ws, response): - - ws.subscribe(INSTRUMENT_TOKENS) - ws.set_mode(ws.MODE_FULL, INSTRUMENT_TOKENS) - printer("Subscribed to token(s) in 'Full' mode", len(INSTRUMENT_TOKENS)) - - -# --------------------------------------------------------------------------------------------------------------------- - - -def start_live_feed_input( - api_key: str, - access_token: str, +def to_string( + python_data, + indent = 4, + default = None, + separators = None, + no_space = False ): - kite_ws = KiteTicker( - api_key = api_key, - access_token = access_token - ) + """ + Converts the given pythonic data to a JSON string. + :param python_data: The input data like a dict. + :param indent: The tab-width for pretty presentation. + :param default: The function to use on something that cannot be directly parsed into a JSON string. + :param separators: Custom separators to use. + :param no_space: If you want a dense JSON string that saves memory by not using spaces or tabs or line-breaks. Not + good for human readability, very good for saving memory. WARNING: THIS OVERRIDES EVERY OTHER PARAMETER EXCEPT + 'default'. + :return: The JSON string representation of the input pythonic data. + """ - # Assign the callbacks: - kite_ws.on_ticks = on_ticks - # kite_ws.on_close = on_close - # kite_ws.on_error = on_error - kite_ws.on_connect = on_connect - # kite_ws.on_reconnect = on_reconnect - # kite_ws.on_noreconnect = on_noreconnect + if no_space: + json_data = json.dumps( + python_data, + default = default, + separators = (',', ':') + ) - # If you choose to go threaded, you will need to work purely with callbacks. - # You will need to have an infinite loop in the main thread. - kite_ws.connect(threaded = True) + else: + json_data = json.dumps( + python_data, + indent = indent, + default = default, + separators = separators + ) + + return json_data # --------------------------------------------------------------------------------------------------------------------- -@sio.event -def subscribe(sid, data): - printer(data) - sio.emit("echo", data) +def from_file(file): + + """ + Reads a JSON file and returns it as a pythonic variable like a dict. + :param file: The path to the file on the disk or a file held in RAM as a BytesIO object. + :return: The decoded pythonic variable. + """ + + if isinstance(file, io.BytesIO): + file.seek(0) + json_data = file.getvalue() + else: json_data = files.read_file(file) + python_data = from_string(json_data) + return python_data + + +# --------------------------------------------------------------------------------------------------------------------- + + +def to_file( + file, + python_data, + indent = 4, + default = None, + separators = None, + no_space = False +): + + """ + + :param file: Either a path to a file on disk, or a buffer in RAM in the form of a BytesIO object. + :param python_data: The pythonic data to be converted to the JSON string. + :param indent: The tab-width for pretty presentation. + :param default: The function to use on something that cannot be directly parsed into a JSON string. + :param separators: Custom separators to use. + :param no_space: If you want a dense JSON string that saves memory by not using spaces or tabs or line-breaks. Not + good for human readability, very good for saving memory. WARNING: THIS OVERRIDES EVERY OTHER PARAMETER EXCEPT + 'default'. + :return: True/False if a path was given, else the same BytesIO object with the written JSON data. + """ + + json_data = to_string( + python_data, + indent = indent, + default = default, + separators = separators, + no_space = no_space + ) + + if isinstance(file, io.BytesIO): + file.write(json_data.encode("utf-8")) + file.seek(0) + return file + + else: + try: + files.write_file(file, json_data, mode = "w") + return True + except: return False # ***************************************************************************************************************** @@ -194,12 +198,4 @@ def subscribe(sid, data): if __name__ == "__main__": - # Connect to Zerodha: - creds = json.from_file(r"../creds/zerodha/api.json") - start_live_feed_input( - api_key = creds["apiKey"], - access_token = creds["accessToken"] - ) - - eventlet.wsgi.server(eventlet.listen(("0.0.0.0", 5214)), app) - + pass diff --git a/controllers_v2/core/auth_token.py b/controllers_v2/core/auth_token.py index c348cea..9ec024b 100644 --- a/controllers_v2/core/auth_token.py +++ b/controllers_v2/core/auth_token.py @@ -156,6 +156,8 @@ class CoreAuthTokenController(CoreBaseModel): mongo_data_conn: AsyncMongo, auth_token: CoreAuthTokenModel, token_notes: dict, + display_name: str = None, + display_picture: str = None, session_token: str = None, ) -> ObjectId: @@ -166,6 +168,8 @@ class CoreAuthTokenController(CoreBaseModel): :param mongo_data_conn: The database connection (MongoDB) to use to perform the action. :param auth_token: An instance of the core auth-token model that holds data in the database. :param token_notes: Any notes to feed into MariaDB with the token identifier. + :param display_name: The name of the user to user as their display name. + :param display_picture: The URL at which you will find a display picture of the user. :param session_token: The session token of the user who requested this service. :return: An ObjectId to later store the granted tokens. """ @@ -230,8 +234,8 @@ class CoreAuthTokenController(CoreBaseModel): auth_token.client, # ............................................ 'p_provider' auth_token.status, # ............................................ 'p_current_status' "Auth Requested", # ............................................. 'p_last_action' - None, # ......................................................... 'p_display_name' - None, # ......................................................... 'p_display_picture' + display_name, # ................................................. 'p_display_name' + display_picture, # .............................................. 'p_display_picture' mongo_json["key"], # ............................................ 'p_token_id' json.to_string(python_data = token_notes, no_space = True), # ... 'p_notes' auth_token.user.userId # ........................................ 'p_created_by' @@ -249,6 +253,8 @@ class CoreAuthTokenController(CoreBaseModel): token_key: ObjectId | str, auth_token: CoreAuthTokenModel, token_notes: dict, + display_name: str = None, + display_picture: str = None, session_token: str = None ) -> bool: @@ -261,6 +267,8 @@ class CoreAuthTokenController(CoreBaseModel): :param token_key: The identifier granted by the 'generate_token_key' method. :param auth_token: The actual auth/token data to be saved to the database. :param token_notes: Any notes to feed into MariaDB with the token identifier. + :param display_name: The name of the user to user as their display name. + :param display_picture: The URL at which you will find a display picture of the user. :param session_token: The session token of the user who requested this service. :return: True if saved, False if failed. """ @@ -320,8 +328,8 @@ class CoreAuthTokenController(CoreBaseModel): mongo_json["client"], # ......................................... 'p_provider' auth_token.status, # ............................................ 'p_current_status' "Auth Granted", # ............................................... 'p_last_action' - auth_token.token.get("displayName"), # .......................... 'p_display_name' - auth_token.token.get("displayPictureUrl"), # .................... 'p_display_picture' + display_name, # ................................................. 'p_display_name' + display_picture, # .............................................. 'p_display_picture' token_key, # .................................................... 'p_token_id' json.to_string(python_data = token_notes, no_space = True), # ... 'p_notes' auth_token.user.userId # ........................................ 'p_created_by' @@ -339,6 +347,8 @@ class CoreAuthTokenController(CoreBaseModel): mongo_data_conn: AsyncMongo, auth_token: CoreAuthTokenModel, token_notes: dict, + display_name: str = None, + display_picture: str = None, session_token: str = None ) -> bool: @@ -349,6 +359,8 @@ class CoreAuthTokenController(CoreBaseModel): :param mongo_data_conn: The database connection (MongoDB) to use to perform the action. :param auth_token: The actual auth/token data to be saved to the database. :param token_notes: Any notes to feed into MariaDB with the token identifier. + :param display_name: The name of the user to user as their display name. + :param display_picture: The URL at which you will find a display picture of the user. :param session_token: The session token of the user who requested this service. :return: True if saved, False if failed. """ @@ -358,11 +370,13 @@ class CoreAuthTokenController(CoreBaseModel): # Get a token id (and receive its key): token_key = await self.generate_token_key( - sql_conn = sql_conn, - mongo_data_conn = mongo_data_conn, - auth_token = auth_token, - token_notes = token_notes, - session_token = session_token + sql_conn = sql_conn, + mongo_data_conn = mongo_data_conn, + auth_token = auth_token, + token_notes = token_notes, + display_name = display_name, + display_picture = display_picture, + session_token = session_token ) # Immediately save the details against that token id: @@ -372,6 +386,8 @@ class CoreAuthTokenController(CoreBaseModel): token_key = token_key, auth_token = auth_token, token_notes = token_notes, + display_name = display_name, + display_picture = display_picture, session_token = session_token ) diff --git a/controllers_v2/finstitutions/trading/zerodha_kite.py b/controllers_v2/finstitutions/trading/zerodha_kite.py index a387135..c20b356 100644 --- a/controllers_v2/finstitutions/trading/zerodha_kite.py +++ b/controllers_v2/finstitutions/trading/zerodha_kite.py @@ -234,7 +234,9 @@ class ZerodhaKiteTradingController(TradingController): token_notes = { "apiKey": auth_token.auth["apiKey"], "authUrl": auth_url - } + }, + display_name = zerodha_auth_token.userId, + display_picture = zerodha_auth_token.displayPictureUrl ) # Done here: @@ -278,6 +280,8 @@ class ZerodhaKiteTradingController(TradingController): symbols_subset = kite.instruments(exchange = exchange) symbols_subset = [TradingSymbol.from_zerodha_kite(s) for s in symbols_subset] symbol_list.data += symbols_subset + print("LEN(EXC):", len(symbols_subset)) + print("LEN(SYM):", len(symbol_list.data)) symbol_list.success = True symbol_list.message = "Symbol list retrieved successfully." diff --git a/kill.sh b/kill.sh index 35ef109..159505a 100644 --- a/kill.sh +++ b/kill.sh @@ -3,7 +3,6 @@ # Kill all the scripts: echo "Killing the script." pkill -9 -f "$(pwd)/api/main.py" -pkill -9 -f "$(pwd)/playground/socketio/tick_simulator.py" # Get the name of the current directory, # and kill the monitors of the above scripts: diff --git a/models/api/finstitutions/trading/symbols/list.py b/models/api/finstitutions/trading/symbols/list.py index 7db6dbb..bfd46d3 100644 --- a/models/api/finstitutions/trading/symbols/list.py +++ b/models/api/finstitutions/trading/symbols/list.py @@ -43,6 +43,9 @@ from typing import Optional, Literal, Union, List from utils_v2.string import regex from utils_v2.date_time import date_time +# Other models: +from models.finstitutions.trading.symbols import TradingSymbol + # To work with date and time: import datetime @@ -75,145 +78,6 @@ REGEX_SESSION_TOKEN = r"^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9] # ***************************************************************************************************************** -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 - ) if instrument["expiry"] else None, - 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( diff --git a/models/finstitutions/__init__.py b/models/finstitutions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/models/finstitutions/trading/__init__.py b/models/finstitutions/trading/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/models/finstitutions/trading/symbols.py b/models/finstitutions/trading/symbols.py new file mode 100644 index 0000000..beaff72 --- /dev/null +++ b/models/finstitutions/trading/symbols.py @@ -0,0 +1,222 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Monday, 23rd Dec., 2024. + + OBJECTIVE: + + To provide a structure to represent trading symbols. + + 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 *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** 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 + ) if instrument["expiry"] else None, + 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 + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/models/finstitutions/trading/ticks.py b/models/finstitutions/trading/ticks.py new file mode 100644 index 0000000..72f2821 --- /dev/null +++ b/models/finstitutions/trading/ticks.py @@ -0,0 +1,476 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Tuesday, 24th Dec., 2024. + + OBJECTIVE: + + To provide a structure to represent trading tick updates. + + 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, computed_field +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 *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +class OneMarketDepth(BaseModel): + + price: float = Field( + description = "a price at which trader(s) are willing to trade this instrument", + frozen = True + ) + + qty: int = Field( + description = "the no. of shares available at the above price", + frozen = True + ) + + orders: int = Field( + description = "how many orders have contributed to the above quantity" + ) + + @computed_field + def liquidity(self) -> float: + return self.price * self.qty + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + +# --------------------------------------------------------------------------------------------------------------------- + + +class MarketDepth(BaseModel): + + buy: List[OneMarketDepth] = Field( + description = "the buying side market depth", + frozen = True + ) + + sell: List[OneMarketDepth] = Field( + description = "the selling side market depth", + frozen = True + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + +# --------------------------------------------------------------------------------------------------------------------- + + +class TradingTick(BaseModel): + + symbol: str = Field( + description = "the symbol of the instrument" + ) + + exchange: Literal["NSE", "NFO", "BSE", "BFO", "MCX", "CDS", "BCD"] = Field( + description = "the exchange on which this instrument 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 + ) + + tradeable: bool = Field( + description = "whether, or not, this instrument is tradeable", + 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 + ) + + 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 | None = Field( + description = "the timezone in which the expiry timestamp my be interpreted; should be compatible with pytz", + frozen = True, + examples = ["UTC", "Asia/Kolkata"] + ) + + ltp: float = Field( + description = "the last price of this instrument at the time of requesting the symbol list", + frozen = True + ) + + chg: float = Field( + description = "the absolute change since the previous close", + frozen=True + ) + + pChg: float = Field( + description = "the percentage change since last close", + frozen = True + ) + + o: float = Field( + description = "this session's open price", + frozen = True + ) + + h: float = Field( + description = "this session's high price", + frozen = True + ) + + l: float = Field( + description = "this session's low price", + frozen = True + ) + + c: float = Field( + description = "this session's close price", + frozen = True + ) + + totVol: int = Field( + description = "the total volume of this instrument that has been traded in this session", + frozen = True + ) + + vwap: float | None = Field( + description = "the volume weighted average price in this session", + frozen = True + ) + + totBuyQty: int = Field( + description = "the total open buy qty. on the exchange for this symbol", + frozen = True + ) + + totSellQty: int = Field( + description = "the total open sell qty. on the exchange for this symbol", + frozen = True + ) + + oi: int | None = Field( + description = "the total open interest of this instrument (if derivative)", + frozen = True + ) + + oiDayHigh: int | None = Field( + description = "this session's highest open interest of this instrument (if derivative)", + frozen = True + ) + + oiDayLow: int | None = Field( + description="this session's lowest open interest of this instrument (if derivative)", + frozen=True + ) + + tradeTs: AwareDatetime | None = Field( + description = "the last trade time (utc) of this instrument", + default = None, + frozen = True + ) + + tradeTz: str = Field( + description = "the timezone in which the last trade time should be interpreted; should be compatible with pytz", + frozen = True, + examples = ["UTC", "Asia/Kolkata"] + ) + + exchgTs: AwareDatetime | None = Field( + description = "the time (utc) at which this update was received from the exchange", + default = None, + frozen = True + ) + + exchgTz: str = Field( + description = "the timezone in which the exchange's time should be interpreted; should be compatible with pytz", + frozen = True, + examples = ["UTC", "Asia/Kolkata"] + ) + + depth: MarketDepth = Field( + description = "the market depth data for this instrument at the time of this update" + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + # ┏┓ ┏┓ + # ┃ ┓┏┏╋┏┓┏┳┓ ┣ ┓┏┏┓┏┏ + # ┗┛┗┻┛┗┗┛┛┗┗ ┻ ┗┻┛┗┗┛ + + @staticmethod + def from_zerodha_kite( + ticks: dict | List[dict], + lookup: dict + ) -> list: + + # Ensure that we are working with a list: + if not isinstance(ticks, list): ticks = [ticks] + + # Iterate through the ticks and fit them into the model: + modelled_ticks = [] + for tick in ticks: + broker_token = tick["instrument_token"] + tick_lookup = lookup[broker_token] + change = tick["change"] + modelled_ticks.append( + TradingTick( + symbol = tick_lookup["symbol"], + exchange = tick_lookup["exchange"], + exchangeToken = tick_lookup["exchangeToken"], + broker = "zerodhaKite", + brokerToken = broker_token, + tradeable = tick["tradeable"], + segment = tick_lookup["segment"], + type = tick_lookup["type"], + expiryTs = tick_lookup["expiryTs"], + expiryTz = tick_lookup["expiryTz"], + ltp = , + chg = change, + pChg = + ) + ) + + # Done here: + return modelled_ticks + + # ┓┏ ┓• ┓ • + # ┃┃┏┓┃┓┏┫┏┓╋┓┏┓┏┓ + # ┗┛┗┻┗┗┗┻┗┻┗┗┗┛┛┗ + + @field_validator( + "expiryTs", + "tradeTs", "exchgTs", + mode = "before" + ) + def parse_date_time(cls, value): + + # If the input is null, + # we can't do anything: + if not value: value = None + + # When the input is a string, we try to parse it: + elif isinstance(value, str): + value = value.strip() + value = date_time.parse_date_time( + input_value = value, + timezone = date_time.TIMEZONE_UTC + ) + + # When the input is a datetime obj., + # we only work on the timezone: + elif isinstance(value, datetime.datetime): + value = date_time.to_timezone( + value, + timezone = date_time.TIMEZONE_UTC + ) + + # Done here: + return value + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + zerodha_tick = { + "tradable": True, + "mode": "full", + "instrument_token": 408065, + "last_price": 1928.25, + "last_traded_quantity": 157, + "average_traded_price": 1942.5, + "volume_traded": 5596931, + "total_buy_quantity": 217126, + "total_sell_quantity": 380092, + "ohlc": { + "open": 1975.15, + "high": 1979.95, + "low": 1911.25, + "close": 1946.2 + }, + "change": -0.9223101428424646, + "last_trade_time": "2024-12-20 14:46:14", + "oi": 0, + "oi_day_high": 0, + "oi_day_low": 0, + "exchange_timestamp": "2024-12-20 14:46:15", + "depth": { + "buy": [ + { + "quantity": 3, + "price": 1928.15, + "orders": 2 + }, + { + "quantity": 6, + "price": 1927.95, + "orders": 2 + }, + { + "quantity": 66, + "price": 1927.7, + "orders": 2 + }, + { + "quantity": 25, + "price": 1927.65, + "orders": 1 + }, + { + "quantity": 194, + "price": 1927.6, + "orders": 6 + } + ], + "sell": [ + { + "quantity": 134, + "price": 1928.25, + "orders": 5 + }, + { + "quantity": 3, + "price": 1928.3, + "orders": 1 + }, + { + "quantity": 560, + "price": 1928.35, + "orders": 2 + }, + { + "quantity": 64, + "price": 1928.4, + "orders": 2 + }, + { + "quantity": 400, + "price": 1928.45, + "orders": 1 + } + ] + } + } + zerodha_lookup = { + 408065: { + "symbol": "MYSTOCK", + "exchange": "NSE", + "exchangeToken": 12345678, + "segment": "NFO-OPT", + "type": "CE" + } + } + + my_ticks = TradingTick.from_zerodha_kite( + ticks = zerodha_tick, + instrument_lookup = zerodha_lookup + ) + + print(my_ticks[0]) diff --git a/playground/socketio/from_kafka.py b/playground/socketio/from_kafka.py new file mode 100644 index 0000000..f880768 --- /dev/null +++ b/playground/socketio/from_kafka.py @@ -0,0 +1,112 @@ +import random +import time +import socketio +import asyncio +import datetime +import requests + +# Create a Socket.IO server instance +sio = socketio.AsyncServer(cors_allowed_origins = "*") + +# Create an aiohttp web application +from aiohttp import web + +app = web.Application() + +# Attach the Socket.IO server to the aiohttp application +sio.attach(app) +from utils_v2.queue.async_kafka import ProducerKafka, ConsumerKafka, get_ssl_context +import os + +# Define the test params: +TOPIC = "kft_file_upload" +BOOTSTRAP_SERVERS = "del.ditscentre.in:9092" +SSL_CONTEXT = get_ssl_context( + ca_file = "../../creds/kafka/cert_authority.pem", + cert_file = "../../creds/kafka/fullchain.pem", + key_file = "../../creds/kafka/privkey.pem" +) +my_consumer = ConsumerKafka( + topic = TOPIC, + bootstrap_servers = BOOTSTRAP_SERVERS, + security_protocol = "SSL", + ssl_context = SSL_CONTEXT +) + + +# Event: Client connects +@sio.event +async def connect(sid, environ): + print(f"Client {sid} connected") + requests.post( + url = r"https://api.thecaoffice.com/converse/tech/alert/chat/backend", + json = { + "type": "info", + "chatClient": "telegram", + "chatId": "-4206946032", + # "chatId": "1275560043", + "message": f"*SocketIO Connected!*\n👍 SID: {sid}" + } + ) + + +# Event: Client disconnects +@sio.event +async def disconnect(sid): + print(f"Client {sid} disconnected") + requests.post( + url = r"https://api.thecaoffice.com/converse/tech/alert/chat/backend", + json = { + "type": "info", + "chatClient": "telegram", + "chatId": "-4206946032", + # "chatId": "1275560043", + "message": f"*SocketIO Disconnected!*\n❌ SID: {sid}" + } + ) + + +@sio.event +async def message(sid, data): + print("MESSAGE:", data) + + +# Function to generate random data +async def broadcast_one_tick(tick): + await sio.emit("ticks", tick) + + +# Function to broadcast data every second asynchronously +async def broadcast_ticks(): + while True: + messages = await my_consumer.consume(count = 100, timeout = 1.0) + print(f"Received {len(messages)} tick(s)") + tasks = [broadcast_one_tick(m["value"]) for m in messages] + if tasks: results = await asyncio.gather(*tasks) + + +# Start broadcasting random data using asyncio +async def start_broadcast(): + await broadcast_ticks() + + +# Main function to run the aiohttp server and the broadcasting +async def main(): + # Start broadcasting random data in the background + asyncio.create_task(start_broadcast()) + + # Run the web server + runner = web.AppRunner(app) + await runner.setup() + site = web.TCPSite(runner, '0.0.0.0', 5214) + print("Server running on http://0.0.0.0:5214") + await site.start() + + # Keep the server running + while True: + await asyncio.sleep(3600) # Keep the server alive for 1 hour or adjust as needed + + +# Run the main asyncio event loop +if __name__ == '__main__': + asyncio.run(main()) diff --git a/playground/socketio/tick_simulator.py b/playground/socketio/tick_simulator.py index 6a4e9a0..fe8e38d 100644 --- a/playground/socketio/tick_simulator.py +++ b/playground/socketio/tick_simulator.py @@ -134,7 +134,7 @@ SYMBOL_TO_PRICE_MAP = { # ***************************************************************************************************************** -# --- Nothing Yet +tg_update = False # ***************************************************************************************************************** @@ -144,21 +144,39 @@ SYMBOL_TO_PRICE_MAP = { # ***************************************************************************************************************** +@sio.event +async def before_connect(sid, environ): + + print(f"Checking connection attempt from {sid}.") + + # # Simulate a failed connection based on some conditions (for example, invalid IP or header) + # user_agent = environ.get('HTTP_USER_AGENT', '') + # if 'BadUserAgent' in user_agent: + # print(f"Rejected connection from {sid} due to invalid User-Agent.") + # return False # This will reject the connection attempt + + return True # Allow connection + + +# --------------------------------------------------------------------------------------------------------------------- + + @sio.event async def connect(sid, environ): print(f"Client {sid} connected") - async with httpx.AsyncClient() as client: - try: await client.post( - url = r"https://api.thecaoffice.com/converse/tech/alert/chat/backend", - json = { - "type": "info", - "chatClient": "telegram", - "chatId": "-4206946032", - # "chatId": "1275560043", - "message": f"*SocketIO Connected!*\n👍 SID: {sid}" - } - ) - except: pass + if tg_update: + async with httpx.AsyncClient() as client: + try: await client.post( + url = r"https://api.thecaoffice.com/converse/tech/alert/chat/backend", + json = { + "type": "info", + "chatClient": "telegram", + "chatId": "-4206946032", + # "chatId": "1275560043", + "message": f"*SocketIO Connected!*\n👍 SID: {sid}" + } + ) + except: pass # --------------------------------------------------------------------------------------------------------------------- @@ -167,18 +185,19 @@ async def connect(sid, environ): @sio.event async def disconnect(sid): print(f"Client {sid} disconnected") - async with httpx.AsyncClient() as client: - try: await client.post( - url = r"https://api.thecaoffice.com/converse/tech/alert/chat/backend", - json = { - "type": "info", - "chatClient": "telegram", - "chatId": "-4206946032", - # "chatId": "1275560043", - "message": f"*SocketIO Disconnected!*\n❌ SID: {sid}" - } - ) - except: pass + if tg_update: + async with httpx.AsyncClient() as client: + try: await client.post( + url = r"https://api.thecaoffice.com/converse/tech/alert/chat/backend", + json = { + "type": "info", + "chatClient": "telegram", + "chatId": "-4206946032", + # "chatId": "1275560043", + "message": f"*SocketIO Disconnected!*\n❌ SID: {sid}" + } + ) + except: pass def round_tick(price): @@ -339,7 +358,6 @@ if __name__ == "__main__": runner = web.AppRunner(app) await runner.setup() site = web.TCPSite(runner, "0.0.0.0", 5214) - print("Server running on http://0.0.0.0:5000") await site.start() # Keep the server running diff --git a/run.sh b/run.sh index c47ade0..702cf79 100644 --- a/run.sh +++ b/run.sh @@ -3,7 +3,6 @@ # Use this to run the microservice without any docker setup. source .venv/bin/activate python3 "$(pwd)/api/main.py" --host "0.0.0.0" --port 5106 --workers 4 --script-id "kps_cnv_KBC3MoaU" & -python3 "$(pwd)/playground/socketio/tick_simulator.py" & deactivate # All done: diff --git a/utils_v2/trading/zerodha_kite/models/auth_tokens.py b/utils_v2/trading/zerodha_kite/models/auth_tokens.py index a22ae1a..4043a7a 100644 --- a/utils_v2/trading/zerodha_kite/models/auth_tokens.py +++ b/utils_v2/trading/zerodha_kite/models/auth_tokens.py @@ -85,9 +85,16 @@ import httpx class ZerodhaKiteAuthTokens(BaseModel): + userId: str = Field( + description = "the id of the user, typically in 'ABC123' format", + frozen = True, + alias = "user_id" + ) + userType: str | None = Field( description = "the type of the user", default = None, + frozen = True, alias = "user_type", examples = ["individual/ind_with_nom"] ) @@ -95,60 +102,71 @@ class ZerodhaKiteAuthTokens(BaseModel): email: str | None = Field( description = "the email id of the user", default = None, + frozen = True, alias = "email" ) name: str = Field( description = "the name of the user", + frozen = True, alias = "user_name" ) displayName: str | None = Field( description = "the short display name of the user", default = None, + frozen = True, alias = "user_shortname" ) displayPictureUrl: str | None = Field( description = "the display picture of the user", default = None, + frozen = True, alias = "avatar_url" ) exchanges: List[str] = Field( description = "the list of exchanges this user can trade on", + frozen = True, alias = "exchanges" ) products: List[str] = Field( description = "the list of products (offered by the broker) this user can avail", + frozen = True, alias = "products" ) orderTypes: List[str] = Field( description = "the list of order types this user can place", + frozen = True, alias = "order_types" ) accessToken: str = Field( description = "the main access token to be used in actual requests", + frozen = True, alias = "access_token" ) refreshToken: str | None = Field( description = "token to be used to refresh the access token; may not be provided", default = None, + frozen = True, alias = "refresh_token" ) publicToken: str | None = Field( description = "undocumented on their official documentation", default = None, + frozen = True, alias = "public_token" ) loginTs: AwareDatetime = Field( description = "the time (utc) at which this user logged in", + frozen = True, alias = "login_time" ) diff --git a/wsocket/finstitutions/trading/live_feed.py b/wsocket/finstitutions/trading/live_feed.py deleted file mode 100644 index 606a752..0000000 --- a/wsocket/finstitutions/trading/live_feed.py +++ /dev/null @@ -1,93 +0,0 @@ -""" - - AUTHOR: - - Khushal P Soonderji - - DATE: - - monday, 23rd Dec., 2024 - - OBJECTIVE: - - To provide a SocketIO app for socket-base communication with the front-end. - - REFERENCES: - - 01. YouTube: https://www.youtube.com/watch?v=H1eLJMC5oTg&t=3s - - DOWNLOADS: - - N/A - -""" - - -# ***************************************************************************************************************** -# ***** **** -# *** IMPORT *** -# ***** **** -# ***************************************************************************************************************** - - -# To make sibling directories accessible for imports: -import sys -sys.path.append(".") -sys.path.append("..") - -# System-level activities: -import io -import os - -# my utils: -from utils_v2.string import json - -# For asynchronous activities: -import asyncio - -# for debugging: -from icecream import IceCreamDebugger - - -# ***************************************************************************************************************** -# ***** **** -# *** MACROS / ONE-TIME INIT *** -# ***** **** -# ***************************************************************************************************************** - - -# Debugging: -printer = IceCreamDebugger(prefix = "SocketIO | ", includeContext = True) - - -# ***************************************************************************************************************** -# ***** **** -# *** VARIABLES *** -# ***** **** -# ***************************************************************************************************************** - - -# --- Nothing Yet - - -# ***************************************************************************************************************** -# ***** **** -# *** FUNCTIONS *** -# ***** **** -# ***************************************************************************************************************** - - -async def subscribe(sid, data): - printer(data) - - -# ***************************************************************************************************************** -# ***** **** -# *** MAIN PROGRAM *** -# ***** **** -# ***************************************************************************************************************** - - -if __name__ == "__main__": - - pass diff --git a/wsocket/main.py b/wsocket/main.py deleted file mode 100644 index 70c2fff..0000000 --- a/wsocket/main.py +++ /dev/null @@ -1,151 +0,0 @@ -""" - - AUTHOR: - - Khushal P Soonderji - - DATE: - - monday, 23rd Dec., 2024 - - OBJECTIVE: - - To provide a SocketIO app for socket-base communication with the front-end. - - REFERENCES: - - 01. YouTube: https://www.youtube.com/watch?v=H1eLJMC5oTg&t=3s - - DOWNLOADS: - - N/A - -""" - - -# ***************************************************************************************************************** -# ***** **** -# *** IMPORT *** -# ***** **** -# ***************************************************************************************************************** - - -# To make sibling directories accessible for imports: -import sys -sys.path.append(".") -sys.path.append("..") - -# System-level activities: -import io -import os - -# my utils: -from utils_v2.string import json - -# To work with SocketIO: -import socketio -from aiohttp import web - -# For asynchronous activities: -import asyncio - -# for debugging: -from icecream import IceCreamDebugger - -# To work with date and time: -import time -import datetime - -# To work with Zerodha's Kite platform: -from kiteconnect import KiteConnect, KiteTicker - - -# ***************************************************************************************************************** -# ***** **** -# *** MACROS / ONE-TIME INIT *** -# ***** **** -# ***************************************************************************************************************** - - -INSTRUMENT_MAP = { - 256265: "NIFTY 50", - 260617: "NIFTY 100", - 259849: "NIFTY IT", - 341249: "HDFCBANK", - 738561: "RELIANCE", - 408065: "INFY", - 2953217: "TCS", - 356865: "HINDUNILVR", - 1270529: "ICICIBANK", - 492033: "KOTAKBANK", - 110630919: "GOLD25JAN75800CE", - 110050823: "SILVER25FEB76000CE", - 10670594: "NIFTY24DEC23650PE", - 17167874: "BANKNIFTY24DEC45000PE", -} -INSTRUMENT_TOKENS = list(INSTRUMENT_MAP.keys()) - - -# ***************************************************************************************************************** -# ***** **** -# *** VARIABLES *** -# ***** **** -# ***************************************************************************************************************** - - -# The SocketIo server: -sio = socketio.AsyncServer(cors_allowed_origins = "*") -app = web.Application() -sio.attach(app) - -# Debugging: -printer = IceCreamDebugger(prefix = "SocketIO | ", includeContext = True) - - -# ***************************************************************************************************************** -# ***** **** -# *** FUNCTIONS *** -# ***** **** -# ***************************************************************************************************************** - - -@sio.event -async def connect(sid, environ): - printer(sid) - - -# --------------------------------------------------------------------------------------------------------------------- - - -@sio.event -async def disconnect(sid): - printer(sid) - - -# ***************************************************************************************************************** -# ***** **** -# *** MAIN PROGRAM *** -# ***** **** -# ***************************************************************************************************************** - - -if __name__ == "__main__": - - # Import the needed events: - from wsocket.finstitutions.trading import live_feed - - async def main(): - - # register all the events: - sio.on("subscribe", live_feed.subscribe) - - # Run the web server: - runner = web.AppRunner(app) - await runner.setup() - site = web.TCPSite(runner, "0.0.0.0", 5214) - await site.start() - - # Keep the server running: - while True: await asyncio.sleep(3_600) - - asyncio.run(main())