From e2976f0dfceeef50e2ac27a2b88aaefd23c57443 Mon Sep 17 00:00:00 2001 From: khushal Date: Mon, 30 Dec 2024 11:43:15 +0530 Subject: [PATCH] (20241230) Paper Trading auth added. --- .../finstitutions/trading/oauth/request.py | 37 ++- api/main.py | 7 + .../finstitutions/trading/paper_trading.py | 227 ++++++++++++++++++ .../api/finstitutions/trading/auth/oauth.py | 31 ++- models/core/auth_token.py | 2 +- 5 files changed, 298 insertions(+), 6 deletions(-) create mode 100644 controllers_v2/finstitutions/trading/paper_trading.py diff --git a/api/blueprints/finstitutions/trading/oauth/request.py b/api/blueprints/finstitutions/trading/oauth/request.py index 1fba50b..c7e964c 100644 --- a/api/blueprints/finstitutions/trading/oauth/request.py +++ b/api/blueprints/finstitutions/trading/oauth/request.py @@ -163,8 +163,41 @@ async def request_oauth_authorization_url( ) # Start by assuming failure: + success = False auth_url = None + # ┏┓ ┏┓ ┏┳┓ ┓• + # ┣ ┏┓┏┓ ┃┃┏┓┏┓┏┓┏┓ ┃ ┏┓┏┓┏┫┓┏┓┏┓ + # ┻ ┗┛┛ ┣┛┗┻┣┛┗ ┛ ┻ ┛ ┗┻┗┻┗┛┗┗┫ + # ┛ ┛ + + if inbound_data.client == "paperTrading": + + # Immediately save the details against that token id: + success = await current_app.paper_trading_controller.set_token_direct( + sql_conn = current_app.sql_writer, + mongo_data_conn = current_app.data_mongo, + auth_token = CoreAuthTokenModel( + serviceType = "stockTrading", + client = inbound_data.client, + authType = "auth", + user = kwargs["session_info"], + clientUserId = { + "username": inbound_data.auth.username + }, + auth = inbound_data.auth.model_dump(), + status = "active", + syncFreq = 1500 + ), + token_notes = { + "username": inbound_data.auth.username + }, + session_token = inbound_headers["X-Session-Token"] + ) + + # Check if things were successful: + if not success: auth_url = None + # ┏┓ ┏┓ ┓┓ ┓┏┓• # ┣ ┏┓┏┓ ┏┛┏┓┏┓┏┓┏┫┣┓┏┓ ┃┫ ┓╋┏┓ # ┻ ┗┛┛ ┗┛┗ ┛ ┗┛┗┻┛┗┗┻ ┛┗┛┗┗┗ @@ -214,8 +247,8 @@ async def request_oauth_authorization_url( # Done here: return ResponseModel( - status_code = StatusCodes.OK if auth_url else StatusCodes.FAILED, - http_code = HttpCodes.SUCCESS if auth_url else HttpCodes.INTERNAL_SERVER_ERROR, + status_code = StatusCodes.OK if success else StatusCodes.FAILED, + http_code = HttpCodes.SUCCESS if success else HttpCodes.INTERNAL_SERVER_ERROR, data = { "client": inbound_data.client, "authorizationUrl": auth_url diff --git a/api/main.py b/api/main.py index fc04967..c127d9c 100644 --- a/api/main.py +++ b/api/main.py @@ -82,6 +82,7 @@ from controllers_v2.message.sms.savvy_bulk_sms_kenya import SavvyBulkSMSKenyaCon # --- from controllers_v2.finstitutions.trading.all_trading import AllTradingController from controllers_v2.finstitutions.trading.zerodha_kite import ZerodhaKiteTradingController +from controllers_v2.finstitutions.trading.paper_trading import PaperTradingController # --- from controllers_v2.finstitutions.payments.all_payments import AllPaymentsController from controllers_v2.finstitutions.payments.safaricom_mpesa_express import SafaricomMPesaExpressPaymentsController @@ -466,6 +467,12 @@ async def app_startup(**kwargs): alert_url = current_app.script_data["alerts"]["url"], debug = enable_debugging ) + current_app.paper_trading_controller = PaperTradingController( + cache = current_app.module_cache, + http_client = current_app.http_client, + alert_url = current_app.script_data["alerts"]["url"], + debug = enable_debugging + ) # Finstitutions / Payments Controllers: current_app.payments_controller = AllPaymentsController( diff --git a/controllers_v2/finstitutions/trading/paper_trading.py b/controllers_v2/finstitutions/trading/paper_trading.py new file mode 100644 index 0000000..e5d3a87 --- /dev/null +++ b/controllers_v2/finstitutions/trading/paper_trading.py @@ -0,0 +1,227 @@ +""" + + AUTHOR: + + Khushal P Soonderji + + DATE: + + Monday, 30th Dec., 2024 + + OBJECTIVE: + + To handle paper trading accounts from here. + + 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 + +# To handle exceptions: +from pydantic import ValidationError + + +# ***************************************************************************************************************** +# ***** **** +# *** MACROS / ONE-TIME INIT *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** VARIABLES *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** FUNCTIONS *** +# ***** **** +# ***************************************************************************************************************** + + +# --- Nothing Yet + + +# ***************************************************************************************************************** +# ***** **** +# *** CLASSES *** +# ***** **** +# ***************************************************************************************************************** + + +class PaperTradingController(TradingController): + + # ┏┓┓ ┓┏ + # ┃ ┃┏┓┏┏ ┃┃┏┓┏┓┏ + # ┗┛┗┗┻┛┛ ┗┛┗┻┛ ┛ + + CLIENT_NAME = "paperTrading" + + # ┏┓ + # ┃ ┏┓┏┓┏╋┏┓┓┏┏╋┏┓┏┓ + # ┗┛┗┛┛┗┛┗┛ ┗┻┗┗┗┛┛ + + def __init__( + self, + cache: AsyncRedisCache = None, + http_client: httpx.AsyncClient = None, + alert_url: str = None, + debug: bool = True, + debug_prefix: str = "Paper Trading (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. + """ + + raise NotImplementedError + + async def handle_authorization_callback( + self, + sql_conn: AsyncMySQL, + mongo_data_conn: AsyncMongo, + inbound_data: dict + ) -> bool: + + """ + Not needed for paper trading. + :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: + + """ + Not needed for paper trading. + :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 + + +# ***************************************************************************************************************** +# ***** **** +# *** MAIN PROGRAM *** +# ***** **** +# ***************************************************************************************************************** + + +if __name__ == "__main__": + + pass diff --git a/models/api/finstitutions/trading/auth/oauth.py b/models/api/finstitutions/trading/auth/oauth.py index dd51f6b..53cc161 100644 --- a/models/api/finstitutions/trading/auth/oauth.py +++ b/models/api/finstitutions/trading/auth/oauth.py @@ -75,6 +75,30 @@ REGEX_SESSION_TOKEN = r"^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9] # ***************************************************************************************************************** +class PaperTradingAuth(BaseModel): + + username: str = Field( + description = "??", + frozen = True + ) + + password: str = Field( + description = "??", + frozen = True + ) + + # ┏┓ ┏• + # ┃ ┏┓┏┓╋┓┏┓ + # ┗┛┗┛┛┗┛┗┗┫ + # ┛ + + class Config: + extra = "forbid" + + +# --------------------------------------------------------------------------------------------------------------------- + + class ZerodhaKiteAuth(BaseModel): apiKey: str = Field( @@ -131,8 +155,8 @@ class TradingAuthRequestHeaders(BaseModel): class TradingAuthRequestData(BaseModel): - client: Literal["zerodhaKite"] = Field(alias = "client") - auth: Union[ZerodhaKiteAuth] + client: Literal["zerodhaKite", "paperTrading"] = Field(alias = "client") + auth: Union[ZerodhaKiteAuth, PaperTradingAuth] # ┏┓ ┏• # ┃ ┏┓┏┓╋┓┏┓ @@ -151,7 +175,8 @@ class TradingAuthRequestData(BaseModel): client = values.client auth = values.auth harmony_map = { - "zerodhaKite": ZerodhaKiteAuth + "zerodhaKite": ZerodhaKiteAuth, + "paperTrading": PaperTradingAuth } if not isinstance(auth, harmony_map[client]): raise ValueError(f"incorrect 'auth' for selected client '{client}'") diff --git a/models/core/auth_token.py b/models/core/auth_token.py index 23fee2e..e36d800 100644 --- a/models/core/auth_token.py +++ b/models/core/auth_token.py @@ -109,7 +109,7 @@ class CoreAuthTokenModel(BaseModel): "telegram", "whatsapp", # .................. Chat Clients "nimbusSmsIndia", "savvyBulkSmsKenya", # ... SMS Clients "razorpay", "safaricomMPesaExpress", # ..... Payment Gateways - "zerodhaKite", # ........................... Stock Brokers + "zerodhaKite", "paperTrading", # ........... Stock Brokers "theCaOfficeAi" # .......................... Software ] = Field( description = "the third-part client that was used",