From 551b55342255c490f91ec8164cd8d078c58a1fd3 Mon Sep 17 00:00:00 2001 From: dhanu Date: Fri, 5 Sep 2025 21:11:05 +0530 Subject: [PATCH 01/16] setup update --- SmartApi/version.py | 2 +- setup.py | 24 +++++++++++++----------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/SmartApi/version.py b/SmartApi/version.py index d7710f04..94de2eb0 100644 --- a/SmartApi/version.py +++ b/SmartApi/version.py @@ -2,7 +2,7 @@ __description__ = "Angel Broking openApi integration" __url__ = "https://www.angelbroking.com/" __download_url__ = "https://github.com/angel-one/smartapi-python" -__version__ = "1.5.3" +__version__ = "1.5.5" __author__ = "ab-smartapi" __token__ = "ab-smartapi" __author_email__ = "smartapi.sdk@gmail.com" diff --git a/setup.py b/setup.py index 6e238b61..5342632e 100644 --- a/setup.py +++ b/setup.py @@ -1,21 +1,20 @@ from setuptools import setup, find_packages -about={} -with open("README.md", "r") as readme_file: +with open("README.md", "r", encoding="utf-8") as readme_file: readme = readme_file.read() requirements = [ - "requests>=2.18.4", - "six>=1.11.0", - "python-dateutil>=2.6.1" - ] + "requests>=2.18.4", + "six>=1.11.0", + "python-dateutil>=2.6.1" +] setup( name="smartapi-python", version="1.5.5", author="ab-smartapi", author_email="smartapi.sdk@gmail.com", - description="Angel Broking openApi integration", + description="Angel Broking openAPI integration", long_description=readme, long_description_content_type="text/markdown", url="https://github.com/angelbroking-github/smartapi-python", @@ -26,13 +25,16 @@ "Intended Audience :: Developers", "Intended Audience :: Financial and Insurance Industry", "Programming Language :: Python", - "Natural Language :: English", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3.5", - "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Natural Language :: English", "Topic :: Office/Business :: Financial :: Investment", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Libraries" ], + python_requires='>=3.7', ) From db21c13ce42eaa487f1ca57712b6760f301cd29e Mon Sep 17 00:00:00 2001 From: dhanu Date: Fri, 5 Sep 2025 21:52:20 +0530 Subject: [PATCH 02/16] Added detailed docstrings and formatting to SmartConnect class --- .gitignore | 5 +- SmartApi/smartConnect.py | 958 ++++++++++++++++++++++----------------- 2 files changed, 534 insertions(+), 429 deletions(-) diff --git a/.gitignore b/.gitignore index 0b843705..092381cf 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,7 @@ .idea/smartapi-python.iml build/* smartapi_python.egg-info -dist/* \ No newline at end of file +dist/* + +*.pyc +__pycache__ \ No newline at end of file diff --git a/SmartApi/smartConnect.py b/SmartApi/smartConnect.py index d39fd53e..0fb30146 100644 --- a/SmartApi/smartConnect.py +++ b/SmartApi/smartConnect.py @@ -1,90 +1,114 @@ -from six.moves.urllib.parse import urljoin import json import logging -import SmartApi.smartExceptions as ex -import requests -from requests import get -import re, uuid +import re import socket -import os -import logzero -from logzero import logger -import time import ssl +import uuid + +import requests +from urllib.parse import urljoin +from requests import get + from SmartApi.version import __version__, __title__ +import SmartApi.smartExceptions as ex + +logger = logging.getLogger(__name__) + + +# ROOTURL = "https://openapisuat.angelbroking.com" +# LOGINURL ="https://smartapi.angelbroking.com/login" + +ROOTURL = "https://apiconnect.angelone.in" #prod endpoint +LOGINURL = "https://smartapi.angelone.in/publisher-login" #prod endpoint + +DEFAULT_TIMEOUT = 10 # (seconds) + +# API endpoints used for authentication and user session management +ROUTES = { + # Authentication endpoints + "api.login": "/rest/auth/angelbroking/user/v1/loginByPassword", + "api.logout": "/rest/secure/angelbroking/user/v1/logout", + "api.token": "/rest/auth/angelbroking/jwt/v1/generateTokens", + "api.refresh": "/rest/auth/angelbroking/jwt/v1/generateTokens", + "api.user.profile": "/rest/secure/angelbroking/user/v1/getProfile", + + # Order related endpoints + "api.order.place": "/rest/secure/angelbroking/order/v1/placeOrder", + "api.order.placefullresponse": "/rest/secure/angelbroking/order/v1/placeOrder", + "api.order.modify": "/rest/secure/angelbroking/order/v1/modifyOrder", + "api.order.cancel": "/rest/secure/angelbroking/order/v1/cancelOrder", + "api.order.book": "/rest/secure/angelbroking/order/v1/getOrderBook", + + # Market data and trades + "api.ltp.data": "/rest/secure/angelbroking/order/v1/getLtpData", + "api.trade.book": "/rest/secure/angelbroking/order/v1/getTradeBook", + "api.rms.limit": "/rest/secure/angelbroking/user/v1/getRMS", + "api.holding": "/rest/secure/angelbroking/portfolio/v1/getHolding", + "api.position": "/rest/secure/angelbroking/order/v1/getPosition", + "api.convert.position": "/rest/secure/angelbroking/order/v1/convertPosition", + + # GTT (Good Till Triggered) endpoints + "api.gtt.create": "/gtt-service/rest/secure/angelbroking/gtt/v1/createRule", + "api.gtt.modify": "/gtt-service/rest/secure/angelbroking/gtt/v1/modifyRule", + "api.gtt.cancel": "/gtt-service/rest/secure/angelbroking/gtt/v1/cancelRule", + "api.gtt.details": "/rest/secure/angelbroking/gtt/v1/ruleDetails", + "api.gtt.list": "/rest/secure/angelbroking/gtt/v1/ruleList", + + # Historical and market data + "api.candle.data": "/rest/secure/angelbroking/historical/v1/getCandleData", + "api.oi.data": "/rest/secure/angelbroking/historical/v1/getOIData", + "api.market.data": "/rest/secure/angelbroking/market/v1/quote", + "api.search.scrip": "/rest/secure/angelbroking/order/v1/searchScrip", + "api.allholding": "/rest/secure/angelbroking/portfolio/v1/getAllHolding", + + # Detailed order info and margin + "api.individual.order.details": "/rest/secure/angelbroking/order/v1/details/", + "api.margin.api": "rest/secure/angelbroking/margin/v1/batch", + "api.estimateCharges": "rest/secure/angelbroking/brokerage/v1/estimateCharges", + + # EDIS (Electronic DIS) related endpoints + "api.verifyDis": "rest/secure/angelbroking/edis/v1/verifyDis", + "api.generateTPIN": "rest/secure/angelbroking/edis/v1/generateTPIN", + "api.getTranStatus": "rest/secure/angelbroking/edis/v1/getTranStatus", + + # Market analytics endpoints + "api.optionGreek": "rest/secure/angelbroking/marketData/v1/optionGreek", + "api.gainersLosers": "rest/secure/angelbroking/marketData/v1/gainersLosers", + "api.putCallRatio": "rest/secure/angelbroking/marketData/v1/putCallRatio", + "api.oIBuildup": "rest/secure/angelbroking/marketData/v1/OIBuildup", + "api.nseIntraday": "rest/secure/angelbroking/marketData/v1/nseIntraday", + "api.bseIntraday": "rest/secure/angelbroking/marketData/v1/bseIntraday", +} -log = logging.getLogger(__name__) class SmartConnect(object): - #_rootUrl = "https://openapisuat.angelbroking.com" - _rootUrl="https://apiconnect.angelone.in" #prod endpoint - #_login_url ="https://smartapi.angelbroking.com/login" - _login_url="https://smartapi.angelone.in/publisher-login" #prod endpoint - _default_timeout = 7 # In seconds - - _routes = { - "api.login":"/rest/auth/angelbroking/user/v1/loginByPassword", - "api.logout":"/rest/secure/angelbroking/user/v1/logout", - "api.token": "/rest/auth/angelbroking/jwt/v1/generateTokens", - "api.refresh": "/rest/auth/angelbroking/jwt/v1/generateTokens", - "api.user.profile": "/rest/secure/angelbroking/user/v1/getProfile", - - "api.order.place": "/rest/secure/angelbroking/order/v1/placeOrder", - "api.order.placefullresponse": "/rest/secure/angelbroking/order/v1/placeOrder", - "api.order.modify": "/rest/secure/angelbroking/order/v1/modifyOrder", - "api.order.cancel": "/rest/secure/angelbroking/order/v1/cancelOrder", - "api.order.book":"/rest/secure/angelbroking/order/v1/getOrderBook", - - "api.ltp.data": "/rest/secure/angelbroking/order/v1/getLtpData", - "api.trade.book": "/rest/secure/angelbroking/order/v1/getTradeBook", - "api.rms.limit": "/rest/secure/angelbroking/user/v1/getRMS", - "api.holding": "/rest/secure/angelbroking/portfolio/v1/getHolding", - "api.position": "/rest/secure/angelbroking/order/v1/getPosition", - "api.convert.position": "/rest/secure/angelbroking/order/v1/convertPosition", - - "api.gtt.create":"/gtt-service/rest/secure/angelbroking/gtt/v1/createRule", - "api.gtt.modify":"/gtt-service/rest/secure/angelbroking/gtt/v1/modifyRule", - "api.gtt.cancel":"/gtt-service/rest/secure/angelbroking/gtt/v1/cancelRule", - "api.gtt.details":"/rest/secure/angelbroking/gtt/v1/ruleDetails", - "api.gtt.list":"/rest/secure/angelbroking/gtt/v1/ruleList", - - "api.candle.data":"/rest/secure/angelbroking/historical/v1/getCandleData", - "api.oi.data":"/rest/secure/angelbroking/historical/v1/getOIData", - "api.market.data":"/rest/secure/angelbroking/market/v1/quote", - "api.search.scrip": "/rest/secure/angelbroking/order/v1/searchScrip", - "api.allholding": "/rest/secure/angelbroking/portfolio/v1/getAllHolding", - - "api.individual.order.details": "/rest/secure/angelbroking/order/v1/details/", - "api.margin.api" : 'rest/secure/angelbroking/margin/v1/batch', - "api.estimateCharges" : 'rest/secure/angelbroking/brokerage/v1/estimateCharges', - "api.verifyDis" : 'rest/secure/angelbroking/edis/v1/verifyDis', - "api.generateTPIN" : 'rest/secure/angelbroking/edis/v1/generateTPIN', - "api.getTranStatus" : 'rest/secure/angelbroking/edis/v1/getTranStatus', - "api.optionGreek" : 'rest/secure/angelbroking/marketData/v1/optionGreek', - "api.gainersLosers" : 'rest/secure/angelbroking/marketData/v1/gainersLosers', - "api.putCallRatio" : 'rest/secure/angelbroking/marketData/v1/putCallRatio', - "api.oIBuildup" : 'rest/secure/angelbroking/marketData/v1/OIBuildup', - "api.nseIntraday" : 'rest/secure/angelbroking/marketData/v1/nseIntraday', - "api.bseIntraday" : 'rest/secure/angelbroking/marketData/v1/bseIntraday', - } - - try: - clientPublicIp= " " + get('https://api.ipify.org').text - if " " in clientPublicIp: - clientPublicIp=clientPublicIp.replace(" ","") - hostname = socket.gethostname() - clientLocalIp=socket.gethostbyname(hostname) - except Exception as e: - logger.error(f"Exception while retriving IP Address,using local host IP address: {e}") - finally: - clientPublicIp="106.193.147.98" - clientLocalIp="127.0.0.1" - clientMacAddress=':'.join(re.findall('..', '%012x' % uuid.getnode())) - accept = "application/json" - userType = "USER" - sourceID = "WEB" - - def __init__(self, api_key=None, access_token=None, refresh_token=None,feed_token=None, userId=None, root=None, debug=False, timeout=None, proxies=None, pool=None, disable_ssl=False,accept=None,userType=None,sourceID=None,Authorization=None,clientPublicIP=None,clientMacAddress=None,clientLocalIP=None,privateKey=None): + + accept: str = "application/json" + userType: str = "USER" + sourceID: str = "WEB" + + def __init__( + self, + api_key: str, + access_token: str | None = None, + refresh_token: str | None = None, + feed_token: str | None = None, + userId: str | None = None, + root: str | None = None, + debug: bool = False, + timeout: int | None = None, + proxies: dict | None = None, + pool: dict | None = None, + disable_ssl: bool = False, + accept: str | None = None, + userType: str | None = None, + sourceID: str | None = None, + Authorization: str | None = None, + clientPublicIP: str | None = None, + clientMacAddress: str | None = None, + clientLocalIP: str | None = None, + privateKey: str | None = None, + ): self.debug = debug self.api_key = api_key self.session_expiry_hook = None @@ -94,25 +118,29 @@ def __init__(self, api_key=None, access_token=None, refresh_token=None,feed_toke self.feed_token = feed_token self.userId = userId self.proxies = proxies if proxies else {} - self.root = root or self._rootUrl - self.timeout = timeout or self._default_timeout - self.Authorization= None - self.clientLocalIP=self.clientLocalIp - self.clientPublicIP=self.clientPublicIp - self.clientMacAddress=self.clientMacAddress - self.privateKey=api_key - self.accept=self.accept - self.userType=self.userType - self.sourceID=self.sourceID - - # Create SSL context + self.root = root or ROOTURL + self.timeout = timeout or DEFAULT_TIMEOUT + self.Authorization = Authorization + + # Initialize client info + _clientPublicIP, _clientLocalIP, _clientMacAddress = self._get_client_info() + + self.clientPublicIP = clientPublicIP or _clientPublicIP + self.clientLocalIP = clientLocalIP or _clientLocalIP + self.clientMacAddress = clientMacAddress or _clientMacAddress + + self.privateKey = privateKey or api_key + self.accept = accept or self.accept + self.userType = userType or self.userType + self.sourceID = sourceID or self.sourceID + + # Create SSL context and configure TLS versions self.ssl_context = ssl.create_default_context() self.ssl_context.options |= ssl.OP_NO_TLSv1 # Disable TLS 1.0 self.ssl_context.options |= ssl.OP_NO_TLSv1_1 # Disable TLS 1.1 - - # Configure minimum TLS version to TLS 1.2 self.ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2 + # Initialize requests session with optional connection pooling if not disable_ssl: self.reqsession = requests.Session() if pool is not None: @@ -121,33 +149,67 @@ def __init__(self, api_key=None, access_token=None, refresh_token=None,feed_toke else: reqadapter = requests.adapters.HTTPAdapter() self.reqsession.mount("https://", reqadapter) - logger.info(f"in pool") - else: - # If SSL is disabled, use the default SSL context - self.reqsession = requests - - # Create a log folder based on the current date - log_folder = time.strftime("%Y-%m-%d", time.localtime()) - log_folder_path = os.path.join("logs", log_folder) # Construct the full path to the log folder - os.makedirs(log_folder_path, exist_ok=True) # Create the log folder if it doesn't exist - log_path = os.path.join(log_folder_path, "app.log") # Construct the full path to the log file - logzero.logfile(log_path, loglevel=logging.ERROR) # Output logs to a date-wise log file - - if pool: - self.reqsession = requests.Session() - reqadapter = requests.adapters.HTTPAdapter(**pool) - self.reqsession.mount("https://", reqadapter) - logger.info(f"in pool") + logger.info("Using connection pool for HTTPS requests.") else: + # SSL disabled — fallback to requests without session self.reqsession = requests - # disable requests SSL warning - requests.packages.urllib3.disable_warnings() - def requestHeaders(self): - return{ - "Content-type":self.accept, - "X-ClientLocalIP": self.clientLocalIp, - "X-ClientPublicIP": self.clientPublicIp, + + @staticmethod + def _get_client_info() -> tuple[str, str, str]: + """ + Retrieve client public IP, local IP, and MAC address. + + Returns: + Tuple containing (public_ip, local_ip, mac_address). + """ + try: + public_ip: str = get("https://api.ipify.org").text.strip() + hostname: str = socket.gethostname() + local_ip: str = socket.gethostbyname(hostname) + except Exception as e: + logger.error( + f"Exception while retrieving IP Address, using localhost IP: {e}" + ) + public_ip = "106.193.147.98" # fallback public IP + local_ip = "127.0.0.1" # fallback local IP + + mac_address: str = ":".join(re.findall("..", f"{uuid.getnode():012x}")) + + return public_ip, local_ip, mac_address + + @property + def getUserId(self) -> str: + """Get the current user ID.""" + return self.userId + + @property + def getfeedToken(self) -> str: + """Get the current feed token.""" + return self.feed_token + + @property + def getaccessToken(self) -> str: + """Get the access token.""" + return self.access_token + + @property + def getrefreshToken(self) -> str: + """Get the refresh token.""" + return self.refresh_token + + @property + def login_url(self) -> str: + """Generate SmartAPI login URL.""" + return "%s?api_key=%s" % (LOGINURL, self.api_key) + + + def requestHeaders(self) -> dict: + """Return HTTP headers for API requests.""" + return { + "Content-type": self.accept, + "X-ClientLocalIP": self.clientLocalIP, + "X-ClientPublicIP": self.clientPublicIP, "X-MACAddress": self.clientMacAddress, "Accept": self.accept, "X-PrivateKey": self.privateKey, @@ -155,418 +217,458 @@ def requestHeaders(self): "X-SourceID": self.sourceID } - def setSessionExpiryHook(self, method): + def setSessionExpiryHook(self, method) -> None: + """Set callback for session expiry event.""" if not callable(method): raise TypeError("Invalid input type. Only functions are accepted.") self.session_expiry_hook = method - - def getUserId(): - return userId - def setUserId(self,id): - self.userId=id - - def setAccessToken(self, access_token): + def setUserId(self, id: str) -> None: + """Set the user ID value.""" + self.userId = id + def setAccessToken(self, access_token: str) -> None: + """Set the access token string.""" self.access_token = access_token - def setRefreshToken(self, refresh_token): - + def setRefreshToken(self, refresh_token: str) -> None: + """Set the refresh token string.""" self.refresh_token = refresh_token - def setFeedToken(self,feedToken): - - self.feed_token=feedToken + def setFeedToken(self, feedToken: str) -> None: + """Set the market feed token.""" + self.feed_token = feedToken - def getfeedToken(self): - return self.feed_token - - def login_url(self): - """Get the remote login url to which a user should be redirected to initiate the login flow.""" - return "%s?api_key=%s" % (self._login_url, self.api_key) - - def _request(self, route, method, parameters=None): - """Make an HTTP request.""" + def _request( + self, + route: str, + method: str, + parameters: dict = None + ) -> dict | bytes: + """Make a low-level HTTP API request.""" params = parameters.copy() if parameters else {} - - uri =self._routes[route].format(**params) - url = urljoin(self.root, uri) + uri = ROUTES[route].format(**params) + url = urljoin(self.root, uri) - # Custom headers headers = self.requestHeaders() if self.access_token: - # set authorization header - - auth_header = self.access_token - headers["Authorization"] = "Bearer {}".format(auth_header) + headers["Authorization"] = f"Bearer {self.access_token}" if self.debug: - log.debug("Request: {method} {url} {params} {headers}".format(method=method, url=url, params=params, headers=headers)) - + logger.debug(f"Request: {method} {url} {params} {headers}") + try: - r = requests.request(method, - url, - data=json.dumps(params) if method in ["POST", "PUT"] else None, - params=json.dumps(params) if method in ["GET", "DELETE"] else None, - headers=headers, - verify=not self.disable_ssl, - allow_redirects=True, - timeout=self.timeout, - proxies=self.proxies) - + r = requests.request( + method, + url, + data=json.dumps(params) if method in ["POST", "PUT"] else None, + params=json.dumps(params) if method in ["GET", "DELETE"] else None, + headers=headers, + verify=not self.disable_ssl, + allow_redirects=True, + timeout=self.timeout, + proxies=self.proxies + ) except Exception as e: - logger.error(f"Error occurred while making a {method} request to {url}. Headers: {headers}, Request: {params}, Response: {e}") + logger.error(f"Error occurred during {method} request to {url}. Exception: {e}") raise e if self.debug: - log.debug("Response: {code} {content}".format(code=r.status_code, content=r.content)) + logger.debug(f"Response: {r.status_code} {r.content}") - # Validate the content type. + # Parse response based on content-type if "json" in headers["Content-type"]: try: data = json.loads(r.content.decode("utf8")) - except ValueError: - raise ex.DataException("Couldn't parse the JSON response received from the server: {content}".format( - content=r.content)) + raise ex.DataException(f"Invalid JSON response: {r.content}") - # api error if data.get("error_type"): - # Call session hook if its registered and TokenException is raised if self.session_expiry_hook and r.status_code == 403 and data["error_type"] == "TokenException": self.session_expiry_hook() - # native errors exp = getattr(ex, data["error_type"], ex.GeneralException) raise exp(data["message"], code=r.status_code) - if data.get("status",False) is False : - logger.error(f"Error occurred while making a {method} request to {url}. Error: {data['message']}. URL: {url}, Headers: {self.requestHeaders()}, Request: {params}, Response: {data}") + + if not data.get("status", False): + logger.error( + f"Request error: {data['message']} | URL: {url} | Params: {params} | Headers: {headers}" + ) + return data + elif "csv" in headers["Content-type"]: return r.content + else: - raise ex.DataException("Unknown Content-type ({content_type}) with response: ({content})".format( - content_type=headers["Content-type"], - content=r.content)) - - def _deleteRequest(self, route, params=None): + raise ex.DataException( + f"Unknown Content-type ({headers['Content-type']}) with response: {r.content}" + ) + + + def _deleteRequest(self, route: str, params: dict = None) -> dict | bytes: """Alias for sending a DELETE request.""" return self._request(route, "DELETE", params) - def _putRequest(self, route, params=None): + + + def _putRequest(self, route: str, params: dict = None) -> dict | bytes: """Alias for sending a PUT request.""" return self._request(route, "PUT", params) - def _postRequest(self, route, params=None): + + + def _postRequest(self, route: str, params: dict = None) -> dict | bytes: """Alias for sending a POST request.""" return self._request(route, "POST", params) - def _getRequest(self, route, params=None): + + + def _getRequest(self, route: str, params: dict = None) -> dict | bytes: """Alias for sending a GET request.""" return self._request(route, "GET", params) - def generateSession(self,clientCode,password,totp): + + def generateSession(self, clientCode: str, password: str, totp: str) -> dict: + """ + Log in to the SmartAPI platform and generate session tokens. + + This method authenticates a user using their `clientCode`, `password`, and `TOTP` (Time-based One-Time Password). + On successful login, it sets access token, refresh token, feed token, and user ID within the session. - params={"clientcode":clientCode,"password":password,"totp":totp} - loginResultObject=self._postRequest("api.login",params) + It also retrieves the user profile and adds authentication tokens to the returned user object. + + Args: + clientCode (str): The user's client code. + password (str): The password for the client code. + totp (str): The time-based OTP for 2FA. + + Returns: + dict: A dictionary containing user profile information and tokens if login is successful. + If login fails, returns the error response from the API. + """ - if loginResultObject['status']==True: - jwtToken=loginResultObject['data']['jwtToken'] - self.setAccessToken(jwtToken) + params = { + "clientcode": clientCode, + "password": password, + "totp": totp + } + + loginResultObject = self._postRequest("api.login", params) + + if loginResultObject['status'] is True: + jwtToken = loginResultObject['data']['jwtToken'] refreshToken = loginResultObject['data']['refreshToken'] feedToken = loginResultObject['data']['feedToken'] + + self.setAccessToken(jwtToken) self.setRefreshToken(refreshToken) self.setFeedToken(feedToken) + user = self.getProfile(refreshToken) + userId = user['data']['clientcode'] + self.setUserId(userId) - id = user['data']['clientcode'] - # id='D88311' - self.setUserId(id) - user['data']['jwtToken'] = "Bearer " + jwtToken + # Enrich user response with auth tokens + user['data']['jwtToken'] = f"Bearer {jwtToken}" user['data']['refreshToken'] = refreshToken user['data']['feedToken'] = feedToken return user else: return loginResultObject - - def terminateSession(self,clientCode): - logoutResponseObject=self._postRequest("api.logout",{"clientcode":clientCode}) - return logoutResponseObject - - def generateToken(self,refresh_token): - response=self._postRequest('api.token',{"refreshToken":refresh_token}) - jwtToken=response['data']['jwtToken'] - feedToken=response['data']['feedToken'] + + + def terminateSession(self, clientCode: str) -> dict: + """Log out the current user session.""" + return self._postRequest("api.logout", {"clientcode": clientCode}) + + + def generateToken(self, refresh_token: str) -> dict: + """Regenerate JWT and feed tokens.""" + response = self._postRequest( + "api.token", {"refreshToken": refresh_token} + ) + jwtToken = response['data']['jwtToken'] + feedToken = response['data']['feedToken'] + self.setFeedToken(feedToken) self.setAccessToken(jwtToken) return response - def renewAccessToken(self): - response =self._postRequest('api.refresh', { + def renewAccessToken(self) -> dict: + """Renew access token using refresh token.""" + response = self._postRequest('api.refresh', { "jwtToken": self.access_token, "refreshToken": self.refresh_token, - }) - - tokenSet={} + tokenSet = {} if "jwtToken" in response: - tokenSet['jwtToken']=response['data']['jwtToken'] - tokenSet['clientcode']=self. userId - tokenSet['refreshToken']=response['data']["refreshToken"] - + tokenSet['jwtToken'] = response['data']['jwtToken'] + tokenSet['clientcode'] = self.userId + tokenSet['refreshToken'] = response['data']["refreshToken"] + return tokenSet - def getProfile(self,refreshToken): - user=self._getRequest("api.user.profile",{"refreshToken":refreshToken}) - return user - - def placeOrder(self,orderparams): - params=orderparams - for k in list(params.keys()): - if params[k] is None : - del(params[k]) - response= self._postRequest("api.order.place", params) - if response is not None and response.get('status', False): - if 'data' in response and response['data'] is not None and 'orderid' in response['data']: - orderResponse = response['data']['orderid'] - return orderResponse - else: - logger.error(f"Invalid response format: {response}") + def getProfile(self, refreshToken: str) -> dict: + """Fetch user profile using refresh token.""" + return self._getRequest("api.user.profile", {"refreshToken": refreshToken}) + + + def placeOrder(self, orderparams: dict) -> str | None: + """Place an order and return order ID.""" + params = {k: v for k, v in orderparams.items() if v is not None} + response = self._postRequest("api.order.place", params) + + if response and response.get('status', False): + if 'data' in response and response['data'] and 'orderid' in response['data']: + return response['data']['orderid'] + logger.error(f"Invalid response format: {response}") else: logger.error(f"API request failed: {response}") return None - def placeOrderFullResponse(self,orderparams): - params=orderparams - for k in list(params.keys()): - if params[k] is None : - del(params[k]) - response= self._postRequest("api.order.placefullresponse", params) - if response is not None and response.get('status', False): - if 'data' in response and response['data'] is not None and 'orderid' in response['data']: - orderResponse = response - return orderResponse - else: - logger.error(f"Invalid response format: {response}") + + def placeOrderFullResponse(self, orderparams: dict) -> dict: + """Place order and return full API response.""" + params = {k: v for k, v in orderparams.items() if v is not None} + response = self._postRequest("api.order.placefullresponse", params) + + if response and response.get('status', False): + if 'data' in response and response['data'] and 'orderid' in response['data']: + return response + logger.error(f"Invalid response format: {response}") else: logger.error(f"API request failed: {response}") + return response - - def modifyOrder(self,orderparams): - params = orderparams - for k in list(params.keys()): - if params[k] is None: - del(params[k]) - orderResponse= self._postRequest("api.order.modify", params) - return orderResponse - - def cancelOrder(self, order_id,variety): - orderResponse= self._postRequest("api.order.cancel", {"variety": variety,"orderid": order_id}) - return orderResponse - - def ltpData(self,exchange,tradingsymbol,symboltoken): - params={ - "exchange":exchange, - "tradingsymbol":tradingsymbol, - "symboltoken":symboltoken + def modifyOrder(self, orderparams: dict) -> dict: + """Modify an existing order.""" + params = {k: v for k, v in orderparams.items() if v is not None} + return self._postRequest("api.order.modify", params) + + + def cancelOrder(self, order_id: str, variety: str) -> dict: + """Cancel an order by ID and variety.""" + return self._postRequest("api.order.cancel", {"variety": variety, "orderid": order_id}) + + + def ltpData( + self, + exchange: str, + tradingsymbol: str, + symboltoken: str + ) -> dict: + """ + Retrieve the latest traded price (LTP) data for a given symbol. + + Args: + exchange (str): The exchange code (e.g., NSE, BSE). + tradingsymbol (str): The trading symbol of the security. + symboltoken (str): The unique token identifier for the symbol. + + Returns: + dict: The response containing LTP data. + """ + params = { + "exchange": exchange, + "tradingsymbol": tradingsymbol, + "symboltoken": symboltoken } - ltpDataResponse= self._postRequest("api.ltp.data",params) - return ltpDataResponse - - def orderBook(self): - orderBookResponse=self._getRequest("api.order.book") - return orderBookResponse + return self._postRequest("api.ltp.data", params) + + + def orderBook(self) -> dict: + """Fetch the current order book.""" + return self._getRequest("api.order.book") + + def tradeBook(self) -> dict: + """Fetch the current trade book.""" + return self._getRequest("api.trade.book") + + def rmsLimit(self) -> dict: + """Fetch user's RMS limit details.""" + return self._getRequest("api.rms.limit") + + def position(self) -> dict: + """Fetch user's open positions.""" + return self._getRequest("api.position") + + def holding(self) -> dict: + """Fetch user's current holdings.""" + return self._getRequest("api.holding") + + def allholding(self) -> dict: + """Fetch all holdings including T1.""" + return self._getRequest("api.allholding") + + + def convertPosition(self, positionParams: dict) -> dict: + """Convert open positions.""" + params = {k: v for k, v in positionParams.items() if v is not None} + return self._postRequest("api.convert.position", params) + + + def gttCreateRule(self, createRuleParams: dict) -> str: + """Create a new GTT rule.""" + params = {k: v for k, v in createRuleParams.items() if v is not None} + response = self._postRequest("api.gtt.create", params) + return response['data']['id'] + + + def gttModifyRule(self, modifyRuleParams: dict) -> str: + """Modify an existing GTT rule.""" + params = {k: v for k, v in modifyRuleParams.items() if v is not None} + response = self._postRequest("api.gtt.modify", params) + return response['data']['id'] + + + def gttCancelRule(self, gttCancelParams: dict) -> dict: + """Cancel a GTT rule.""" + params = {k: v for k, v in gttCancelParams.items() if v is not None} + return self._postRequest("api.gtt.cancel", params) + + + def gttDetails(self, id: str) -> dict: + """Fetch details of a GTT rule.""" + return self._postRequest("api.gtt.details", {"id": id}) + + + def gttLists(self, status: list, page: int, count: int) -> dict | str: + """List GTT rules by status.""" + if isinstance(status, list): + params = {"status": status, "page": page, "count": count} + return self._postRequest("api.gtt.list", params) + return ( + f"The status param is {type(status)}. " + f"Please use a list like status=['CANCELLED']" + ) - def tradeBook(self): - tradeBookResponse=self._getRequest("api.trade.book") - return tradeBookResponse - - def rmsLimit(self): - rmsLimitResponse= self._getRequest("api.rms.limit") - return rmsLimitResponse - - def position(self): - positionResponse= self._getRequest("api.position") - return positionResponse - def holding(self): - holdingResponse= self._getRequest("api.holding") - return holdingResponse - - def allholding(self): - allholdingResponse= self._getRequest("api.allholding") - return allholdingResponse - - def convertPosition(self,positionParams): - params=positionParams - for k in list(params.keys()): - if params[k] is None: - del(params[k]) - convertPositionResponse= self._postRequest("api.convert.position",params) - - return convertPositionResponse - - def gttCreateRule(self,createRuleParams): - params=createRuleParams - for k in list(params.keys()): - if params[k] is None: - del(params[k]) - - createGttRuleResponse=self._postRequest("api.gtt.create",params) - return createGttRuleResponse['data']['id'] - - def gttModifyRule(self,modifyRuleParams): - params=modifyRuleParams - for k in list(params.keys()): - if params[k] is None: - del(params[k]) - modifyGttRuleResponse=self._postRequest("api.gtt.modify",params) - return modifyGttRuleResponse['data']['id'] - - def gttCancelRule(self,gttCancelParams): - params=gttCancelParams - for k in list(params.keys()): - if params[k] is None: - del(params[k]) - cancelGttRuleResponse=self._postRequest("api.gtt.cancel",params) - return cancelGttRuleResponse - - def gttDetails(self,id): - params={ - "id":id - } - gttDetailsResponse=self._postRequest("api.gtt.details",params) - return gttDetailsResponse - - def gttLists(self,status,page,count): - if type(status)== list: - params={ - "status":status, - "page":page, - "count":count - } - gttListResponse=self._postRequest("api.gtt.list",params) - return gttListResponse - else: - message="The status param is entered as" +str(type(status))+". Please enter status param as a list i.e., status=['CANCELLED']" - return message - - def getCandleData(self,historicDataParams): - params=historicDataParams - for k in list(params.keys()): - if params[k] is None: - del(params[k]) - getCandleDataResponse=self._postRequest("api.candle.data",historicDataParams) - return getCandleDataResponse - - def getOIData(self,historicOIDataParams): - params=historicOIDataParams - for k in list(params.keys()): - if params[k] is None: - del(params[k]) - getOIDataResponse=self._postRequest("api.oi.data",historicOIDataParams) - return getOIDataResponse - - def getMarketData(self,mode,exchangeTokens): - params={ - "mode":mode, - "exchangeTokens":exchangeTokens + def getCandleData(self, historicDataParams: dict) -> dict: + """ + Fetch historical candlestick data for a given symbol and time range. + + Args: + historicDataParams (dict): Parameters for the request, including symbol, + interval, start and end time, etc. + + Returns: + dict: The API response containing historical candle data. + """ + params = { + k: v for k, v in historicDataParams.items() if v is not None } - marketDataResult=self._postRequest("api.market.data",params) - return marketDataResult - - def searchScrip(self, exchange, searchscrip): + return self._postRequest("api.candle.data", params) + + + def getOIData(self, historicOIDataParams: dict) -> dict: + """Fetch historical open interest data.""" + params = {k: v for k, v in historicOIDataParams.items() if v is not None} + return self._postRequest("api.oi.data", params) + + def getMarketData(self, mode: str, exchangeTokens: dict) -> dict: + """Get market data for given exchange tokens.""" + params = { + "mode": mode, + "exchangeTokens": exchangeTokens + } + return self._postRequest("api.market.data", params) + + def searchScrip(self, exchange: str, searchscrip: str) -> dict: + """Search for a scrip in an exchange.""" params = { "exchange": exchange, "searchscrip": searchscrip } - searchScripResult = self._postRequest("api.search.scrip", params) - if searchScripResult["status"] is True and searchScripResult["data"]: - message = f"Search successful. Found {len(searchScripResult['data'])} trading symbols for the given query:" - symbols = "" - for index, item in enumerate(searchScripResult["data"], start=1): - symbol_info = f"{index}. exchange: {item['exchange']}, tradingsymbol: {item['tradingsymbol']}, symboltoken: {item['symboltoken']}" - symbols += "\n" + symbol_info - logger.info(message + symbols) - return searchScripResult - elif searchScripResult["status"] is True and not searchScripResult["data"]: - logger.info("Search successful. No matching trading symbols found for the given query.") - return searchScripResult - else: - return searchScripResult - - def make_authenticated_get_request(self, url, access_token): + result = self._postRequest("api.search.scrip", params) + + if result["status"] and result["data"]: + message = f"Found {len(result['data'])} trading symbols:" + symbols = "\n".join([ + f"{i + 1}. exchange: {item['exchange']}, " + f"tradingsymbol: {item['tradingsymbol']}, " + f"symboltoken: {item['symboltoken']}" + for i, item in enumerate(result["data"]) + ]) + + logger.info(message + "\n" + symbols) + elif result["status"] and not result["data"]: + logger.info("Search successful. No symbols found.") + return result + + + def make_authenticated_get_request(self, url: str, access_token: str) -> dict | None: + """Make a GET request with auth header.""" headers = self.requestHeaders() if access_token: headers["Authorization"] = "Bearer " + access_token response = requests.get(url, headers=headers) if response.status_code == 200: - data = json.loads(response.text) - return data - else: - logger.error(f"Error in make_authenticated_get_request: {response.status_code}") - return None - - def individual_order_details(self, qParam): - url = self._rootUrl + self._routes["api.individual.order.details"] + qParam + return json.loads(response.text) + logger.error(f"GET request failed: {response.status_code}") + return None + + + def individual_order_details(self, qParam: str) -> dict | None: + """Fetch details for an individual order.""" + url = ROOTURL + ROUTES["api.individual.order.details"] + qParam try: - response_data = self.make_authenticated_get_request(url, self.access_token) - return response_data + return self.make_authenticated_get_request(url, self.access_token) except Exception as e: - logger.error(f"Error occurred in ind_order_details: {e}") + logger.error(f"Error in individual_order_details: {e}") return None - - def getMarginApi(self,params): - marginApiResult=self._postRequest("api.margin.api",params) - return marginApiResult - - def estimateCharges(self,params): - estimateChargesResponse=self._postRequest("api.estimateCharges",params) - return estimateChargesResponse - - def verifyDis(self,params): - verifyDisResponse=self._postRequest("api.verifyDis",params) - return verifyDisResponse - - def generateTPIN(self,params): - generateTPINResponse=self._postRequest("api.generateTPIN",params) - return generateTPINResponse - - def getTranStatus(self,params): - getTranStatusResponse=self._postRequest("api.getTranStatus",params) - return getTranStatusResponse - - def optionGreek(self,params): - optionGreekResponse=self._postRequest("api.optionGreek",params) - return optionGreekResponse - - def gainersLosers(self,params): - gainersLosersResponse=self._postRequest("api.gainersLosers",params) - return gainersLosersResponse - - def putCallRatio(self): - putCallRatioResponse=self._getRequest("api.putCallRatio") - return putCallRatioResponse - def nseIntraday(self): - nseIntraday=self._getRequest("api.nseIntraday") - return nseIntraday - - def bseIntraday(self): - bseIntraday=self._getRequest("api.bseIntraday") - return bseIntraday - def oIBuildup(self,params): - oIBuildupResponse=self._postRequest("api.oIBuildup",params) - return oIBuildupResponse - - - def _user_agent(self): - return (__title__ + "-python/").capitalize() + __version__ + def getMarginApi(self, params: dict) -> dict: + """Get margin info for user and symbols.""" + return self._postRequest("api.margin.api", params) + + def estimateCharges(self, params: dict) -> dict: + """Estimate brokerage and tax charges.""" + return self._postRequest("api.estimateCharges", params) + + def verifyDis(self, params: dict) -> dict: + """Verify DIS (depository instruction slip).""" + return self._postRequest("api.verifyDis", params) + + def generateTPIN(self, params: dict) -> dict: + """Generate TPIN for DIS authorization.""" + return self._postRequest("api.generateTPIN", params) + + def getTranStatus(self, params: dict) -> dict: + """Check transaction status for EDIS.""" + return self._postRequest("api.getTranStatus", params) + + def optionGreek(self, params: dict) -> dict: + """Fetch option greeks data.""" + return self._postRequest("api.optionGreek", params) + + def gainersLosers(self, params: dict) -> dict: + """Get top gainers and losers.""" + return self._postRequest("api.gainersLosers", params) + + def putCallRatio(self) -> dict: + """Get put-call ratio data.""" + return self._getRequest("api.putCallRatio") + + def nseIntraday(self) -> dict: + """Fetch intraday data for NSE.""" + return self._getRequest("api.nseIntraday") + + def bseIntraday(self) -> dict: + """Fetch intraday data for BSE.""" + return self._getRequest("api.bseIntraday") + + def oIBuildup(self, params: dict) -> dict: + """Fetch Open Interest buildup data.""" + return self._postRequest("api.oIBuildup", params) + def _user_agent(self) -> str: + """Return custom user agent string.""" + return (__title__ + "-python/").capitalize() + __version__ From 2d146d31a6f15c4fbcc0102cec0146db63e8b73a Mon Sep 17 00:00:00 2001 From: dhanu Date: Fri, 5 Sep 2025 22:04:49 +0530 Subject: [PATCH 03/16] updated docstring --- SmartApi/smartConnect.py | 212 +++++++++++++++++++++++++++++---------- 1 file changed, 159 insertions(+), 53 deletions(-) diff --git a/SmartApi/smartConnect.py b/SmartApi/smartConnect.py index 0fb30146..397b7441 100644 --- a/SmartApi/smartConnect.py +++ b/SmartApi/smartConnect.py @@ -180,32 +180,44 @@ def _get_client_info() -> tuple[str, str, str]: @property def getUserId(self) -> str: - """Get the current user ID.""" + """ + Get the current user ID. + """ return self.userId @property def getfeedToken(self) -> str: - """Get the current feed token.""" + """ + Get the current feed token. + """ return self.feed_token @property def getaccessToken(self) -> str: - """Get the access token.""" + """ + Get the access token. + """ return self.access_token @property def getrefreshToken(self) -> str: - """Get the refresh token.""" + """ + Get the refresh token. + """ return self.refresh_token @property def login_url(self) -> str: - """Generate SmartAPI login URL.""" + """ + Generate SmartAPI login URL. + """ return "%s?api_key=%s" % (LOGINURL, self.api_key) def requestHeaders(self) -> dict: - """Return HTTP headers for API requests.""" + """ + Return HTTP headers for API requests. + """ return { "Content-type": self.accept, "X-ClientLocalIP": self.clientLocalIP, @@ -218,25 +230,35 @@ def requestHeaders(self) -> dict: } def setSessionExpiryHook(self, method) -> None: - """Set callback for session expiry event.""" + """ + Set callback for session expiry event. + """ if not callable(method): raise TypeError("Invalid input type. Only functions are accepted.") self.session_expiry_hook = method def setUserId(self, id: str) -> None: - """Set the user ID value.""" + """ + Set the user ID value. + """ self.userId = id def setAccessToken(self, access_token: str) -> None: - """Set the access token string.""" + """ + Set the access token string. + """ self.access_token = access_token def setRefreshToken(self, refresh_token: str) -> None: - """Set the refresh token string.""" + """ + Set the refresh token string. + """ self.refresh_token = refresh_token def setFeedToken(self, feedToken: str) -> None: - """Set the market feed token.""" + """ + Set the market feed token. + """ self.feed_token = feedToken @@ -246,7 +268,9 @@ def _request( method: str, parameters: dict = None ) -> dict | bytes: - """Make a low-level HTTP API request.""" + """ + Make a low-level HTTP API request. + """ params = parameters.copy() if parameters else {} uri = ROUTES[route].format(**params) @@ -310,22 +334,30 @@ def _request( def _deleteRequest(self, route: str, params: dict = None) -> dict | bytes: - """Alias for sending a DELETE request.""" + """ + Alias for sending a DELETE request. + """ return self._request(route, "DELETE", params) def _putRequest(self, route: str, params: dict = None) -> dict | bytes: - """Alias for sending a PUT request.""" + """ + Alias for sending a PUT request. + """ return self._request(route, "PUT", params) def _postRequest(self, route: str, params: dict = None) -> dict | bytes: - """Alias for sending a POST request.""" + """ + Alias for sending a POST request. + """ return self._request(route, "POST", params) def _getRequest(self, route: str, params: dict = None) -> dict | bytes: - """Alias for sending a GET request.""" + """ + Alias for sending a GET request. + """ return self._request(route, "GET", params) @@ -380,12 +412,16 @@ def generateSession(self, clientCode: str, password: str, totp: str) -> dict: def terminateSession(self, clientCode: str) -> dict: - """Log out the current user session.""" + """ + Log out the current user session. + """ return self._postRequest("api.logout", {"clientcode": clientCode}) def generateToken(self, refresh_token: str) -> dict: - """Regenerate JWT and feed tokens.""" + """ + Regenerate JWT and feed tokens. + """ response = self._postRequest( "api.token", {"refreshToken": refresh_token} ) @@ -398,7 +434,9 @@ def generateToken(self, refresh_token: str) -> dict: return response def renewAccessToken(self) -> dict: - """Renew access token using refresh token.""" + """ + Renew access token using refresh token. + """ response = self._postRequest('api.refresh', { "jwtToken": self.access_token, "refreshToken": self.refresh_token, @@ -413,12 +451,16 @@ def renewAccessToken(self) -> dict: return tokenSet def getProfile(self, refreshToken: str) -> dict: - """Fetch user profile using refresh token.""" + """ + Fetch user profile using refresh token. + """ return self._getRequest("api.user.profile", {"refreshToken": refreshToken}) def placeOrder(self, orderparams: dict) -> str | None: - """Place an order and return order ID.""" + """ + Place an order and return order ID. + """ params = {k: v for k, v in orderparams.items() if v is not None} response = self._postRequest("api.order.place", params) @@ -432,7 +474,9 @@ def placeOrder(self, orderparams: dict) -> str | None: def placeOrderFullResponse(self, orderparams: dict) -> dict: - """Place order and return full API response.""" + """ + Place order and return full API response. + """ params = {k: v for k, v in orderparams.items() if v is not None} response = self._postRequest("api.order.placefullresponse", params) @@ -447,13 +491,17 @@ def placeOrderFullResponse(self, orderparams: dict) -> dict: def modifyOrder(self, orderparams: dict) -> dict: - """Modify an existing order.""" + """ + Modify an existing order. + """ params = {k: v for k, v in orderparams.items() if v is not None} return self._postRequest("api.order.modify", params) def cancelOrder(self, order_id: str, variety: str) -> dict: - """Cancel an order by ID and variety.""" + """ + Cancel an order by ID and variety. + """ return self._postRequest("api.order.cancel", {"variety": variety, "orderid": order_id}) @@ -483,63 +531,87 @@ def ltpData( def orderBook(self) -> dict: - """Fetch the current order book.""" + """ + Fetch the current order book. + """ return self._getRequest("api.order.book") def tradeBook(self) -> dict: - """Fetch the current trade book.""" + """ + Fetch the current trade book. + """ return self._getRequest("api.trade.book") def rmsLimit(self) -> dict: - """Fetch user's RMS limit details.""" + """ + Fetch user's RMS limit details. + """ return self._getRequest("api.rms.limit") def position(self) -> dict: - """Fetch user's open positions.""" + """ + Fetch user's open positions. + """ return self._getRequest("api.position") def holding(self) -> dict: - """Fetch user's current holdings.""" + """ + Fetch user's current holdings. + """ return self._getRequest("api.holding") def allholding(self) -> dict: - """Fetch all holdings including T1.""" + """ + Fetch all holdings including T1. + """ return self._getRequest("api.allholding") def convertPosition(self, positionParams: dict) -> dict: - """Convert open positions.""" + """ + Convert open positions. + """ params = {k: v for k, v in positionParams.items() if v is not None} return self._postRequest("api.convert.position", params) def gttCreateRule(self, createRuleParams: dict) -> str: - """Create a new GTT rule.""" + """ + Create a new GTT rule. + """ params = {k: v for k, v in createRuleParams.items() if v is not None} response = self._postRequest("api.gtt.create", params) return response['data']['id'] def gttModifyRule(self, modifyRuleParams: dict) -> str: - """Modify an existing GTT rule.""" + """ + Modify an existing GTT rule. + """ params = {k: v for k, v in modifyRuleParams.items() if v is not None} response = self._postRequest("api.gtt.modify", params) return response['data']['id'] def gttCancelRule(self, gttCancelParams: dict) -> dict: - """Cancel a GTT rule.""" + """ + Cancel a GTT rule. + """ params = {k: v for k, v in gttCancelParams.items() if v is not None} return self._postRequest("api.gtt.cancel", params) def gttDetails(self, id: str) -> dict: - """Fetch details of a GTT rule.""" + """ + Fetch details of a GTT rule. + """ return self._postRequest("api.gtt.details", {"id": id}) def gttLists(self, status: list, page: int, count: int) -> dict | str: - """List GTT rules by status.""" + """ + List GTT rules by status. + """ if isinstance(status, list): params = {"status": status, "page": page, "count": count} return self._postRequest("api.gtt.list", params) @@ -568,12 +640,16 @@ def getCandleData(self, historicDataParams: dict) -> dict: def getOIData(self, historicOIDataParams: dict) -> dict: - """Fetch historical open interest data.""" + """ + Fetch historical open interest data. + """ params = {k: v for k, v in historicOIDataParams.items() if v is not None} return self._postRequest("api.oi.data", params) def getMarketData(self, mode: str, exchangeTokens: dict) -> dict: - """Get market data for given exchange tokens.""" + """ + Get market data for given exchange tokens. + """ params = { "mode": mode, "exchangeTokens": exchangeTokens @@ -581,7 +657,9 @@ def getMarketData(self, mode: str, exchangeTokens: dict) -> dict: return self._postRequest("api.market.data", params) def searchScrip(self, exchange: str, searchscrip: str) -> dict: - """Search for a scrip in an exchange.""" + """ + Search for a scrip in an exchange. + """ params = { "exchange": exchange, "searchscrip": searchscrip @@ -604,7 +682,9 @@ def searchScrip(self, exchange: str, searchscrip: str) -> dict: def make_authenticated_get_request(self, url: str, access_token: str) -> dict | None: - """Make a GET request with auth header.""" + """ + Make a GET request with auth header. + """ headers = self.requestHeaders() if access_token: headers["Authorization"] = "Bearer " + access_token @@ -616,7 +696,9 @@ def make_authenticated_get_request(self, url: str, access_token: str) -> dict | def individual_order_details(self, qParam: str) -> dict | None: - """Fetch details for an individual order.""" + """ + Fetch details for an individual order. + """ url = ROOTURL + ROUTES["api.individual.order.details"] + qParam try: return self.make_authenticated_get_request(url, self.access_token) @@ -626,49 +708,73 @@ def individual_order_details(self, qParam: str) -> dict | None: def getMarginApi(self, params: dict) -> dict: - """Get margin info for user and symbols.""" + """ + Get margin info for user and symbols. + """ return self._postRequest("api.margin.api", params) def estimateCharges(self, params: dict) -> dict: - """Estimate brokerage and tax charges.""" + """ + Estimate brokerage and tax charges. + """ return self._postRequest("api.estimateCharges", params) def verifyDis(self, params: dict) -> dict: - """Verify DIS (depository instruction slip).""" + """ + Verify DIS (depository instruction slip). + """ return self._postRequest("api.verifyDis", params) def generateTPIN(self, params: dict) -> dict: - """Generate TPIN for DIS authorization.""" + """ + Generate TPIN for DIS authorization. + """ return self._postRequest("api.generateTPIN", params) def getTranStatus(self, params: dict) -> dict: - """Check transaction status for EDIS.""" + """ + Check transaction status for EDIS. + """ return self._postRequest("api.getTranStatus", params) def optionGreek(self, params: dict) -> dict: - """Fetch option greeks data.""" + """ + Fetch option greeks data. + """ return self._postRequest("api.optionGreek", params) def gainersLosers(self, params: dict) -> dict: - """Get top gainers and losers.""" + """ + Get top gainers and losers. + """ return self._postRequest("api.gainersLosers", params) def putCallRatio(self) -> dict: - """Get put-call ratio data.""" + """ + Get put-call ratio data. + """ return self._getRequest("api.putCallRatio") def nseIntraday(self) -> dict: - """Fetch intraday data for NSE.""" + """ + Fetch intraday data for NSE. + """ return self._getRequest("api.nseIntraday") def bseIntraday(self) -> dict: - """Fetch intraday data for BSE.""" + """ + Fetch intraday data for BSE. + """ return self._getRequest("api.bseIntraday") def oIBuildup(self, params: dict) -> dict: - """Fetch Open Interest buildup data.""" + """ + Fetch Open Interest buildup data. + """ return self._postRequest("api.oIBuildup", params) def _user_agent(self) -> str: - """Return custom user agent string.""" + """ + Return custom user agent string. + """ return (__title__ + "-python/").capitalize() + __version__ From 075cebdcc73543f1dc871323a2a15cb7fed9ced3 Mon Sep 17 00:00:00 2001 From: dhanu Date: Fri, 5 Sep 2025 22:39:03 +0530 Subject: [PATCH 04/16] Add detailed docstring with type annotations --- SmartApi/__init__.py | 5 +- SmartApi/smartExceptions.py | 35 +++- SmartApi/smartWebSocketV2.py | 330 ++++++++++++++++++++++++----------- 3 files changed, 259 insertions(+), 111 deletions(-) diff --git a/SmartApi/__init__.py b/SmartApi/__init__.py index 8e9ecc04..8d6dd924 100644 --- a/SmartApi/__init__.py +++ b/SmartApi/__init__.py @@ -1,10 +1,9 @@ from __future__ import unicode_literals,absolute_import from SmartApi.smartConnect import SmartConnect -# from SmartApi.webSocket import WebSocket -from SmartApi.smartApiWebsocket import SmartWebSocket +from SmartApi.smartWebSocketV2 import SmartWebSocketV2 -__all__ = ["SmartConnect","SmartWebSocket"] +__all__ = ["SmartConnect", "SmartWebSocketV2"] diff --git a/SmartApi/smartExceptions.py b/SmartApi/smartExceptions.py index b9f25619..61d45e22 100644 --- a/SmartApi/smartExceptions.py +++ b/SmartApi/smartExceptions.py @@ -7,7 +7,10 @@ def __init__(self, message, code=500): class GeneralException(SmartAPIException): - """An unclassified, general error. Default code is 500.""" + """ + An unclassified, general error. + Default code is 500. + """ def __init__(self, message, code=500): """Initialize the exception.""" @@ -15,7 +18,10 @@ def __init__(self, message, code=500): class TokenException(SmartAPIException): - """Represents all token and authentication related errors. Default code is 403.""" + """ + Represents all token and authentication related errors. + Default code is 403. + """ def __init__(self, message, code=403): """Initialize the exception.""" @@ -23,7 +29,10 @@ def __init__(self, message, code=403): class PermissionException(SmartAPIException): - """Represents permission denied exceptions for certain calls. Default code is 403.""" + """ + Represents permission denied exceptions for certain calls. + Default code is 403. + """ def __init__(self, message, code=403): """Initialize the exception.""" @@ -31,7 +40,10 @@ def __init__(self, message, code=403): class OrderException(SmartAPIException): - """Represents all order placement and manipulation errors. Default code is 500.""" + """ + Represents all order placement and manipulation errors. + Default code is 500. + """ def __init__(self, message, code=500): """Initialize the exception.""" @@ -39,7 +51,10 @@ def __init__(self, message, code=500): class InputException(SmartAPIException): - """Represents user input errors such as missing and invalid parameters. Default code is 400.""" + """ + Represents user input errors such as missing and invalid parameters. + Default code is 400. + """ def __init__(self, message, code=400): """Initialize the exception.""" @@ -47,7 +62,10 @@ def __init__(self, message, code=400): class DataException(SmartAPIException): - """Represents a bad response from the backend Order Management System (OMS). Default code is 502.""" + """ + Represents a bad response from the backend Order Management System (OMS). + Default code is 502. + """ def __init__(self, message, code=502): """Initialize the exception.""" @@ -55,7 +73,10 @@ def __init__(self, message, code=502): class NetworkException(SmartAPIException): - """Represents a network issue between api and the backend Order Management System (OMS). Default code is 503.""" + """ + Represents a network issue between api and the backend Order Management System (OMS). + Default code is 503. + """ def __init__(self, message, code=503): """Initialize the exception.""" diff --git a/SmartApi/smartWebSocketV2.py b/SmartApi/smartWebSocketV2.py index 5f4262fa..b0a5cfab 100644 --- a/SmartApi/smartWebSocketV2.py +++ b/SmartApi/smartWebSocketV2.py @@ -1,18 +1,25 @@ +import logging import struct import time import ssl import json -import websocket -import os -import logging -import logzero -from logzero import logger + +from websocket import WebSocketApp + +logger = logging.getLogger(__name__) + class SmartWebSocketV2(object): """ SmartAPI Web Socket version 2 + + This class manages a WebSocket connection to the SmartAPI + for real-time market data streaming. It supports subscription, + message parsing, and automatic handling of control messages + like ping/pong. """ + ROOT_URI = "wss://smartapisocket.angelone.in/smart-stream" HEART_BEAT_MESSAGE = "ping" HEART_BEAT_INTERVAL = 10 # Adjusted to 10s @@ -51,20 +58,33 @@ class SmartWebSocketV2(object): input_request_dict = {} current_retry_attempt = 0 - def __init__(self, auth_token, api_key, client_code, feed_token, max_retry_attempt=1,retry_strategy=0, retry_delay=10, retry_multiplier=2, retry_duration=60): + def __init__( + self, + auth_token: str, + api_key: str, + client_code: str, + feed_token: str, + max_retry_attempt: int = 1, + retry_strategy: int = 0, + retry_delay: int = 10, + retry_multiplier: int = 2, + retry_duration: int = 60 + ): """ - Initialise the SmartWebSocketV2 instance - Parameters - ------ - auth_token: string - jwt auth token received from Login API - api_key: string - api key from Smart API account - client_code: string - angel one account id - feed_token: string - feed token received from Login API + Initialize the SmartWebSocketV2 instance. + + Parameters + ---------- + auth_token : str + JWT auth token received from the Login API. + api_key : str + API key from the Smart API account. + client_code : str + Angel One account ID. + feed_token : str + Feed token received from the Login API. """ + self.auth_token = auth_token self.api_key = api_key self.client_code = client_code @@ -76,27 +96,27 @@ def __init__(self, auth_token, api_key, client_code, feed_token, max_retry_attem self.retry_delay = retry_delay self.retry_multiplier = retry_multiplier self.retry_duration = retry_duration - # Create a log folder based on the current date - log_folder = time.strftime("%Y-%m-%d", time.localtime()) - log_folder_path = os.path.join("logs", log_folder) # Construct the full path to the log folder - os.makedirs(log_folder_path, exist_ok=True) # Create the log folder if it doesn't exist - log_path = os.path.join(log_folder_path, "app.log") # Construct the full path to the log file - logzero.logfile(log_path, loglevel=logging.INFO) # Output logs to a date-wise log file + if not self._sanity_check(): logger.error("Invalid initialization parameters. Provide valid values for all the tokens.") raise Exception("Provide valid value for all the tokens") - - def _sanity_check(self): + + def _sanity_check(self) -> bool: + """ + Check if essential tokens and codes are present. + """ if not all([self.auth_token, self.api_key, self.client_code, self.feed_token]): return False return True - def _on_message(self, wsapp, message): + def _on_message(self, wsapp: WebSocketApp, message: str): + """ + Handle incoming WebSocket message. + """ logger.info(f"Received message: {message}") if message != "pong": parsed_message = self._parse_binary_data(message) - # Check if it's a control message (e.g., heartbeat) if self._is_control_message(parsed_message): self._handle_control_message(parsed_message) else: @@ -104,43 +124,75 @@ def _on_message(self, wsapp, message): else: self.on_message(wsapp, message) - def _is_control_message(self, parsed_message): + + def _is_control_message(self, parsed_message: dict) -> bool: + """ + Determine if the message is a control message. + """ return "subscription_mode" not in parsed_message - def _handle_control_message(self, parsed_message): + + def _handle_control_message(self, parsed_message: dict): + """ + Process control messages like ping/pong. + """ if parsed_message["subscription_mode"] == 0: self._on_pong(self.wsapp, "pong") elif parsed_message["subscription_mode"] == 1: self._on_ping(self.wsapp, "ping") - # Invoke on_control_message callback with the control message data if hasattr(self, 'on_control_message'): self.on_control_message(self.wsapp, parsed_message) - def _on_data(self, wsapp, data, data_type, continue_flag): + + def _on_data( + self, + wsapp: WebSocketApp, + data: bytes, + data_type: int, + continue_flag + ): + """ + Handle data messages with specific type. + """ if data_type == 2: parsed_message = self._parse_binary_data(data) self.on_data(wsapp, parsed_message) - def _on_open(self, wsapp): + def _on_open(self, wsapp: WebSocketApp): + """ + Handle WebSocket open event. + """ if self.RESUBSCRIBE_FLAG: self.resubscribe() else: self.on_open(wsapp) - def _on_pong(self, wsapp, data): + def _on_pong(self, wsapp: WebSocketApp, data: str): + """ + Handle pong message from server. + """ if data == self.HEART_BEAT_MESSAGE: timestamp = time.time() formatted_timestamp = time.strftime("%d-%m-%y %H:%M:%S", time.localtime(timestamp)) logger.info(f"In on pong function ==> {data}, Timestamp: {formatted_timestamp}") self.last_pong_timestamp = timestamp - def _on_ping(self, wsapp, data): + def _on_ping(self, wsapp: WebSocketApp, data: str): + """ + Handle ping message from server. + """ timestamp = time.time() formatted_timestamp = time.strftime("%d-%m-%y %H:%M:%S", time.localtime(timestamp)) logger.info(f"In on ping function ==> {data}, Timestamp: {formatted_timestamp}") self.last_ping_timestamp = timestamp - def subscribe(self, correlation_id, mode, token_list): + + def subscribe( + self, + correlation_id: str, + mode: int, + token_list: list[dict] + ) -> None: """ This Function subscribe the price data for the given token Parameters @@ -211,8 +263,14 @@ def subscribe(self, correlation_id, mode, token_list): except Exception as e: logger.error(f"Error occurred during subscribe: {e}") raise e - - def unsubscribe(self, correlation_id, mode, token_list): + + + def unsubscribe( + self, + correlation_id: str, + mode: int, + token_list: list[dict] + ) -> None: """ This function unsubscribe the data for given token Parameters @@ -260,7 +318,12 @@ def unsubscribe(self, correlation_id, mode, token_list): logger.error(f"Error occurred during unsubscribe: {e}") raise e - def resubscribe(self): + + def resubscribe(self) -> None: + """ + Resubscribe to all WebSocket streams + Based on saved subscription requests. + """ try: for key, val in self.input_request_dict.items(): token_list = [] @@ -282,9 +345,10 @@ def resubscribe(self): logger.error(f"Error occurred during resubscribe: {e}") raise e + def connect(self): """ - Make the web socket connection with the server + Make the web socket connection with the server """ headers = { "Authorization": self.auth_token, @@ -294,15 +358,25 @@ def connect(self): } try: - self.wsapp = websocket.WebSocketApp(self.ROOT_URI, header=headers, on_open=self._on_open, - on_error=self._on_error, on_close=self._on_close, on_data=self._on_data, - on_ping=self._on_ping, - on_pong=self._on_pong) - self.wsapp.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE}, ping_interval=self.HEART_BEAT_INTERVAL) + self.wsapp = WebSocketApp( + self.ROOT_URI, + header=headers, + on_open=self._on_open, + on_error=self._on_error, + on_close=self._on_close, + on_data=self._on_data, + on_ping=self._on_ping, + on_pong=self._on_pong + ) + self.wsapp.run_forever( + sslopt={"cert_reqs": ssl.CERT_NONE}, + ping_interval=self.HEART_BEAT_INTERVAL + ) except Exception as e: logger.error(f"Error occurred during WebSocket connection: {e}") raise e + def close_connection(self): """ Closes the connection @@ -312,11 +386,18 @@ def close_connection(self): if self.wsapp: self.wsapp.close() - def _on_error(self, wsapp, error): + def _on_error(self, wsapp: WebSocketApp, error: Exception): + """ + Handle WebSocket error events. + """ self.RESUBSCRIBE_FLAG = True if self.current_retry_attempt < self.MAX_RETRY_ATTEMPT: - logger.warning(f"Attempting to resubscribe/reconnect (Attempt {self.current_retry_attempt + 1})...") + logger.warning( + f"Attempting to resubscribe/reconnect (Attempt " + f"{self.current_retry_attempt + 1})..." + ) self.current_retry_attempt += 1 + if self.retry_strategy == 0: #retry_strategy for simple time.sleep(self.retry_delay) elif self.retry_strategy == 1: #retry_strategy for exponential @@ -325,6 +406,7 @@ def _on_error(self, wsapp, error): else: logger.error(f"Invalid retry strategy {self.retry_strategy}") raise Exception(f"Invalid retry strategy {self.retry_strategy}") + try: self.close_connection() self.connect() @@ -336,15 +418,25 @@ def _on_error(self, wsapp, error): self.close_connection() if hasattr(self, 'on_error'): self.on_error("Max retry attempt reached", "Connection closed") - if self.retry_duration is not None and (self.last_pong_timestamp is not None and time.time() - self.last_pong_timestamp > self.retry_duration * 60): + + if self.retry_duration is not None and ( + self.last_pong_timestamp is not None and \ + time.time() - self.last_pong_timestamp > self.retry_duration * 60 + ): logger.warning("Connection closed due to inactivity.") else: - logger.warning("Connection closed due to max retry attempts reached.") + logger.warning( + "Connection closed due to max retry attempts reached." + ) - def _on_close(self, wsapp): + def _on_close(self, wsapp: WebSocketApp) -> None: + """ + Handle WebSocket close event. + """ self.on_close(wsapp) - def _parse_binary_data(self, binary_data): + + def _parse_binary_data(self, binary_data: bytes) -> dict: parsed_data = { "subscription_mode": self._unpack_data(binary_data, 0, 1, byte_format="B")[0], "exchange_type": self._unpack_data(binary_data, 1, 2, byte_format="B")[0], @@ -353,39 +445,52 @@ def _parse_binary_data(self, binary_data): "exchange_timestamp": self._unpack_data(binary_data, 35, 43, byte_format="q")[0], "last_traded_price": self._unpack_data(binary_data, 43, 51, byte_format="q")[0] } + try: - parsed_data["subscription_mode_val"] = self.SUBSCRIPTION_MODE_MAP.get(parsed_data["subscription_mode"]) - - if parsed_data["subscription_mode"] in [self.QUOTE, self.SNAP_QUOTE]: - parsed_data["last_traded_quantity"] = self._unpack_data(binary_data, 51, 59, byte_format="q")[0] - parsed_data["average_traded_price"] = self._unpack_data(binary_data, 59, 67, byte_format="q")[0] - parsed_data["volume_trade_for_the_day"] = self._unpack_data(binary_data, 67, 75, byte_format="q")[0] - parsed_data["total_buy_quantity"] = self._unpack_data(binary_data, 75, 83, byte_format="d")[0] - parsed_data["total_sell_quantity"] = self._unpack_data(binary_data, 83, 91, byte_format="d")[0] - parsed_data["open_price_of_the_day"] = self._unpack_data(binary_data, 91, 99, byte_format="q")[0] - parsed_data["high_price_of_the_day"] = self._unpack_data(binary_data, 99, 107, byte_format="q")[0] - parsed_data["low_price_of_the_day"] = self._unpack_data(binary_data, 107, 115, byte_format="q")[0] - parsed_data["closed_price"] = self._unpack_data(binary_data, 115, 123, byte_format="q")[0] - - if parsed_data["subscription_mode"] == self.SNAP_QUOTE: - parsed_data["last_traded_timestamp"] = self._unpack_data(binary_data, 123, 131, byte_format="q")[0] - parsed_data["open_interest"] = self._unpack_data(binary_data, 131, 139, byte_format="q")[0] - parsed_data["open_interest_change_percentage"] = self._unpack_data(binary_data, 139, 147, byte_format="q")[0] - parsed_data["upper_circuit_limit"] = self._unpack_data(binary_data, 347, 355, byte_format="q")[0] - parsed_data["lower_circuit_limit"] = self._unpack_data(binary_data, 355, 363, byte_format="q")[0] - parsed_data["52_week_high_price"] = self._unpack_data(binary_data, 363, 371, byte_format="q")[0] - parsed_data["52_week_low_price"] = self._unpack_data(binary_data, 371, 379, byte_format="q")[0] + sub_mode = parsed_data["subscription_mode"] + parsed_data["subscription_mode_val"] = self.SUBSCRIPTION_MODE_MAP.get(sub_mode) + + if sub_mode in [self.QUOTE, self.SNAP_QUOTE]: + # Define offsets and formats in a list of tuples for cleaner unpacking + fields = [ + ("last_traded_quantity", 51, 59, "q"), + ("average_traded_price", 59, 67, "q"), + ("volume_trade_for_the_day", 67, 75, "q"), + ("total_buy_quantity", 75, 83, "d"), + ("total_sell_quantity", 83, 91, "d"), + ("open_price_of_the_day", 91, 99, "q"), + ("high_price_of_the_day", 99, 107, "q"), + ("low_price_of_the_day", 107, 115, "q"), + ("closed_price", 115, 123, "q"), + ] + for field, start, end, fmt in fields: + parsed_data[field] = self._unpack_data(binary_data, start, end, byte_format=fmt)[0] + + if sub_mode == self.SNAP_QUOTE: + snap_fields = [ + ("last_traded_timestamp", 123, 131, "q"), + ("open_interest", 131, 139, "q"), + ("open_interest_change_percentage", 139, 147, "q"), + ("upper_circuit_limit", 347, 355, "q"), + ("lower_circuit_limit", 355, 363, "q"), + ("52_week_high_price", 363, 371, "q"), + ("52_week_low_price", 371, 379, "q"), + ] + for field, start, end, fmt in snap_fields: + parsed_data[field] = self._unpack_data(binary_data, start, end, byte_format=fmt)[0] + best_5_buy_and_sell_data = self._parse_best_5_buy_and_sell_data(binary_data[147:347]) - parsed_data["best_5_buy_data"] = best_5_buy_and_sell_data["best_5_sell_data"] - parsed_data["best_5_sell_data"] = best_5_buy_and_sell_data["best_5_buy_data"] - - if parsed_data["subscription_mode"] == self.DEPTH: - parsed_data.pop("sequence_number", None) - parsed_data.pop("last_traded_price", None) - parsed_data.pop("subscription_mode_val", None) - parsed_data["packet_received_time"]=self._unpack_data(binary_data, 35, 43, byte_format="q")[0] - depth_data_start_index = 43 - depth_20_data = self._parse_depth_20_buy_and_sell_data(binary_data[depth_data_start_index:]) + # Fix swapped assignment (seems like a bug in original) + parsed_data["best_5_buy_data"] = best_5_buy_and_sell_data["best_5_buy_data"] + parsed_data["best_5_sell_data"] = best_5_buy_and_sell_data["best_5_sell_data"] + + if sub_mode == self.DEPTH: + # Remove irrelevant keys + for key in ["sequence_number", "last_traded_price", "subscription_mode_val"]: + parsed_data.pop(key, None) + + parsed_data["packet_received_time"] = self._unpack_data(binary_data, 35, 43, byte_format="q")[0] + depth_20_data = self._parse_depth_20_buy_and_sell_data(binary_data[43:]) parsed_data["depth_20_buy_data"] = depth_20_data["depth_20_buy_data"] parsed_data["depth_20_sell_data"] = depth_20_data["depth_20_sell_data"] @@ -394,7 +499,13 @@ def _parse_binary_data(self, binary_data): logger.error(f"Error occurred during binary data parsing: {e}") raise e - def _unpack_data(self, binary_data, start, end, byte_format="I"): + def _unpack_data( + self, + binary_data: bytes, + start: int, + end: int, + byte_format: str = "I" + ) -> tuple: """ Unpack Binary Data to the integer according to the specified byte_format. This function returns the tuple @@ -410,19 +521,15 @@ def _parse_token_value(binary_packet): token += chr(binary_packet[i]) return token - def _parse_best_5_buy_and_sell_data(self, binary_data): - def split_packets(binary_packets): - packets = [] - - i = 0 - while i < len(binary_packets): - packets.append(binary_packets[i: i + 20]) - i += 20 - return packets + def _parse_best_5_buy_and_sell_data(self, binary_data: bytes) -> dict: + """ + Parse binary data to extract top 5 buy and sell order details. + """ + def split_packets(binary_packets: bytes) -> list[bytes]: + return [binary_packets[i:i + 20] for i in range(0, len(binary_packets), 20)] best_5_buy_sell_packets = split_packets(binary_data) - best_5_buy_data = [] best_5_sell_data = [] @@ -444,7 +551,11 @@ def split_packets(binary_packets): "best_5_sell_data": best_5_sell_data } - def _parse_depth_20_buy_and_sell_data(self, binary_data): + + def _parse_depth_20_buy_and_sell_data(self, binary_data: bytes) -> dict: + """ + Parse binary data to extract depth data for top 20 buy and sell orders. + """ depth_20_buy_data = [] depth_20_sell_data = [] @@ -452,14 +563,12 @@ def _parse_depth_20_buy_and_sell_data(self, binary_data): buy_start_idx = i * 10 sell_start_idx = 200 + i * 10 - # Parse buy data buy_packet_data = { "quantity": self._unpack_data(binary_data, buy_start_idx, buy_start_idx + 4, byte_format="i")[0], "price": self._unpack_data(binary_data, buy_start_idx + 4, buy_start_idx + 8, byte_format="i")[0], "num_of_orders": self._unpack_data(binary_data, buy_start_idx + 8, buy_start_idx + 10, byte_format="h")[0], } - # Parse sell data sell_packet_data = { "quantity": self._unpack_data(binary_data, sell_start_idx, sell_start_idx + 4, byte_format="i")[0], "price": self._unpack_data(binary_data, sell_start_idx + 4, sell_start_idx + 8, byte_format="i")[0], @@ -474,20 +583,39 @@ def _parse_depth_20_buy_and_sell_data(self, binary_data): "depth_20_sell_data": depth_20_sell_data } - def on_message(self, wsapp, message): + + def on_message(self, wsapp: WebSocketApp, message: dict) -> None: + """ + Handle incoming text messages from the WebSocket. + """ pass - def on_data(self, wsapp, data): + def on_data(self, wsapp: WebSocketApp, data: dict) -> None: + """ + Handle incoming binary data from the WebSocket. + """ pass - def on_control_message(self, wsapp, message): + def on_control_message(self, wsapp: WebSocketApp, message: dict) -> None: + """ + Handle WebSocket control messages like ping/pong. + """ pass - def on_close(self, wsapp): + def on_close(self, wsapp: WebSocketApp) -> None: + """ + Handle WebSocket connection closure. + """ pass - def on_open(self, wsapp): + def on_open(self, wsapp: WebSocketApp) -> None: + """ + Handle WebSocket connection open event. + """ pass - def on_error(self): + def on_error(self, error: Exception) -> None: + """ + Handle WebSocket errors. + """ pass From 7a2753ba32bea4a4a39de27c4bb6c0c17c2ce8f5 Mon Sep 17 00:00:00 2001 From: dhanu Date: Fri, 5 Sep 2025 22:55:50 +0530 Subject: [PATCH 05/16] added centralized logger to output logs to 'logs/app.log' and console --- SmartApi/loggerConfig.py | 44 ++++++++++++++++++++++++++++++++++++ SmartApi/smartConnect.py | 4 ++-- SmartApi/smartWebSocketV2.py | 6 ++--- 3 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 SmartApi/loggerConfig.py diff --git a/SmartApi/loggerConfig.py b/SmartApi/loggerConfig.py new file mode 100644 index 00000000..6243b1a0 --- /dev/null +++ b/SmartApi/loggerConfig.py @@ -0,0 +1,44 @@ +import logging +import os + +LOG_DIR = "logs" +LOG_FILE = "app.log" + +# Ensure the log directory exists +os.makedirs(LOG_DIR, exist_ok=True) + +def get_logger(name: str, level: str = "WARNING") -> logging.Logger: + """ + Creates and returns a logger instance that logs messages at the specified level and above. + Logs are saved in logs/app.log and also printed to console. + + Parameters: + name (str): Logger name. + level (str): Logging level (e.g. 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). + Defaults to 'WARNING'. + + Returns: + logging.Logger: Configured logger instance. + """ + + logger = logging.getLogger(name) + logger.setLevel(getattr(logging, level.upper(), logging.WARNING)) + + if not logger.hasHandlers(): + # File handler + log_path = os.path.join(LOG_DIR, LOG_FILE) + file_handler = logging.FileHandler(log_path) + file_handler.setLevel(getattr(logging, level.upper(), logging.WARNING)) + + # Console handler + console_handler = logging.StreamHandler() + console_handler.setLevel(getattr(logging, level.upper(), logging.WARNING)) + + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + file_handler.setFormatter(formatter) + console_handler.setFormatter(formatter) + + logger.addHandler(file_handler) + logger.addHandler(console_handler) + + return logger diff --git a/SmartApi/smartConnect.py b/SmartApi/smartConnect.py index 397b7441..75136efc 100644 --- a/SmartApi/smartConnect.py +++ b/SmartApi/smartConnect.py @@ -1,5 +1,4 @@ import json -import logging import re import socket import ssl @@ -10,9 +9,10 @@ from requests import get from SmartApi.version import __version__, __title__ +from SmartApi.loggerConfig import get_logger import SmartApi.smartExceptions as ex -logger = logging.getLogger(__name__) +logger = get_logger(__name__, level="WARNING") # ROOTURL = "https://openapisuat.angelbroking.com" diff --git a/SmartApi/smartWebSocketV2.py b/SmartApi/smartWebSocketV2.py index b0a5cfab..4461b785 100644 --- a/SmartApi/smartWebSocketV2.py +++ b/SmartApi/smartWebSocketV2.py @@ -1,12 +1,12 @@ -import logging import struct import time import ssl import json - from websocket import WebSocketApp -logger = logging.getLogger(__name__) +from SmartApi.loggerConfig import get_logger + +logger = get_logger(__name__, level="WARNING") class SmartWebSocketV2(object): From 0ecc5f7107b03ba4ee8d95583ecb5de88012eaf1 Mon Sep 17 00:00:00 2001 From: dhanu Date: Sat, 6 Sep 2025 10:57:23 +0530 Subject: [PATCH 06/16] updated docstring and fixed --- SmartApi/smartConnect.py | 69 ++++++++++++ SmartApi/smartWebSocketOrderUpdate.py | 154 ++++++++++++++++++++------ SmartApi/smartWebSocketV2.py | 73 ++++++++++-- 3 files changed, 249 insertions(+), 47 deletions(-) diff --git a/SmartApi/smartConnect.py b/SmartApi/smartConnect.py index 75136efc..fd0372f4 100644 --- a/SmartApi/smartConnect.py +++ b/SmartApi/smartConnect.py @@ -82,6 +82,15 @@ class SmartConnect(object): + """ + Main client class for interacting with the Angel One Smart API. + + This class handles session management, authentication, and configuration + required to interact with Angel One REST and WebSocket APIs. It supports + token-based authentication, proxy configuration, SSL handling, and + user metadata for secure access. + + """ accept: str = "application/json" userType: str = "USER" @@ -109,6 +118,66 @@ def __init__( clientLocalIP: str | None = None, privateKey: str | None = None, ): + + """ + Initialize the SmartConnect instance. + + This constructor sets up the Smart API client with all the necessary + authentication tokens, user information, and configuration needed to + interact with Angel One's REST and WebSocket APIs. It allows you to + manage sessions, make authenticated requests, subscribe to live market + data, and customize connection behavior through proxies, SSL options, + and user metadata. + + + Parameters + ---------- + api_key : str + The Smart API key associated with the developer account. + access_token : str, optional + Access token received upon successful login. + refresh_token : str, optional + Refresh token used to renew the session without re-authentication. + feed_token : str, optional + Token used for subscribing to real-time market data feeds. + userId : str, optional + Unique identifier of the user (usually Angel One client ID). + root : str, optional + Base URL for the Smart API (defaults to the production endpoint). + debug : bool, optional + Enable verbose debug logging (default is False). + timeout : int, optional + Timeout value (in seconds) for HTTP requests (default is 7 seconds). + proxies : dict, optional + Dictionary of HTTP/HTTPS proxy settings (if needed for requests). + pool : dict, optional + Connection pool configuration (currently unused/reserved). + disable_ssl : bool, optional + Disable SSL verification (default is False). Useful for debugging. + accept : str, optional + Overrides the default `accept` header ("application/json"). + userType : str, optional + Overrides the default user type ("USER"). + sourceID : str, optional + Overrides the default source identifier ("WEB"). + Authorization : str, optional + Bearer or JWT token to authorize API requests directly. + clientPublicIP : str, optional + Public IP address of the client (used for audit/tracking). + clientMacAddress : str, optional + MAC address of the client device (optional metadata). + clientLocalIP : str, optional + Local IP address of the client machine. + privateKey : str, optional + RSA private key string used for payload encryption (optional). + + Returns + ------- + None + This constructor does not return a value. + + """ + self.debug = debug self.api_key = api_key self.session_expiry_hook = None diff --git a/SmartApi/smartWebSocketOrderUpdate.py b/SmartApi/smartWebSocketOrderUpdate.py index 1f63368b..dbf0ee67 100644 --- a/SmartApi/smartWebSocketOrderUpdate.py +++ b/SmartApi/smartWebSocketOrderUpdate.py @@ -1,87 +1,168 @@ import ssl -import websocket import time -import logging -from logzero import logger -import logzero -import os - -class SmartWebSocketOrderUpdate(object): - WEBSOCKET_URI = "wss://tns.angelone.in/smart-order-update" - HEARTBEAT_MESSAGE = "ping" # Heartbeat message to maintain Socket connection. - HEARTBEAT_INTERVAL_SECONDS = 10 # Interval for sending heartbeat messages to keep the connection alive. - MAX_CONNECTION_RETRY_ATTEMPTS = 2 # Max retry attempts to establish Socket connection in case of failure. - RETRY_DELAY_SECONDS = 10 # Delay between retry attempts when reconnecting to Socket in case of failure. - wsapp = None #Socket connection instance - last_pong_timestamp = None #Timestamp of the last received pong message - current_retry_attempt = 0 #Current retry attempt count - - def __init__(self, auth_token, api_key, client_code, feed_token): + +import websocket +from websocket import WebSocketApp + +from SmartApi.loggerConfig import get_logger + +logger = get_logger(__name__, level="WARNING") + + +class SmartWebSocketOrderUpdate: + """ + WebSocket client for receiving real-time order updates. + """ + + WEBSOCKET_URI: str = "wss://tns.angelone.in/smart-order-update" + HEARTBEAT_MESSAGE: str = "ping" # Heartbeat message to maintain connection + HEARTBEAT_INTERVAL_SECONDS: int = 10 # Interval for heartbeat messages + MAX_CONNECTION_RETRY_ATTEMPTS: int = 2 # Max retry attempts for connection + RETRY_DELAY_SECONDS: int = 10 # Delay between retry attempts + + wsapp = None # WebSocket connection instance + last_pong_timestamp = None # Timestamp of last received pong message + current_retry_attempt = 0 # Current retry attempt count + + def __init__( + self, + auth_token: str, + api_key: str, + client_code: str, + feed_token: str + ): + """ + Initialize SmartWebSocketOrderUpdate with credentials. + + Parameters + ---------- + auth_token : str + Bearer token for WebSocket authorization. + api_key : str + Smart API key. + client_code : str + Angel One client code (user ID). + feed_token : str + Feed token for order update WebSocket. + """ self.auth_token = auth_token self.api_key = api_key self.client_code = client_code self.feed_token = feed_token - # Create a log folder based on the current date - log_folder = time.strftime("%Y-%m-%d", time.localtime()) - log_folder_path = os.path.join("logs", log_folder) # Construct the full path to the log folder - os.makedirs(log_folder_path, exist_ok=True) # Create the log folder if it doesn't exist - log_path = os.path.join(log_folder_path, "app.log") # Construct the full path to the log file - logzero.logfile(log_path, loglevel=logging.INFO) # Output logs to a date-wise log file - - def on_message(self, wsapp, message): + + def on_message(self, wsapp, message: dict): + """ + Handle incoming WebSocket message. + """ logger.info("Received message: %s", message) def on_data(self, wsapp, message, data_type, continue_flag): + """ + Handle incoming WebSocket data frame. + """ self.on_message(wsapp, message) def on_open(self, wsapp): + """ + Handle WebSocket connection open event. + """ logger.info("Connection opened") def on_error(self, wsapp, error): + """ + Handle WebSocket error event. + """ logger.error("Error: %s", error) def on_close(self, wsapp, close_status_code, close_msg): + """ + Handle WebSocket connection close event.""" logger.info("Connection closed") self.retry_connect() def on_ping(self, wsapp, data): + """ + Handle WebSocket ping event. + """ timestamp = time.time() - formatted_timestamp = time.strftime("%d-%m-%y %H:%M:%S", time.localtime(timestamp)) - logger.info("In on ping function ==> %s, Timestamp: %s", data, formatted_timestamp) + formatted_timestamp = time.strftime( + "%d-%m-%y %H:%M:%S", time.localtime(timestamp) + ) + logger.info( + "In on ping function ==> %s, Timestamp: %s", + data, + formatted_timestamp + ) def on_pong(self, wsapp, data): + """ + Handle WebSocket pong event. + """ if data == self.HEARTBEAT_MESSAGE: timestamp = time.time() - formatted_timestamp = time.strftime("%d-%m-%y %H:%M:%S", time.localtime(timestamp)) - logger.info("In on pong function ==> %s, Timestamp: %s", data, formatted_timestamp) + formatted_timestamp = time.strftime( + "%d-%m-%y %H:%M:%S", time.localtime(timestamp) + ) + logger.info( + "In on pong function ==> %s, Timestamp: %s", + data, + formatted_timestamp + ) self.last_pong_timestamp = timestamp else: self.on_data(wsapp, data, websocket.ABNF.OPCODE_BINARY, False) def check_connection_status(self): + """ + Check if connection is alive based on pong timestamp. + """ current_time = time.time() - if self.last_pong_timestamp is not None and current_time - self.last_pong_timestamp > 2 * self.HEARTBEAT_INTERVAL_SECONDS: + if ( + self.last_pong_timestamp is not None and + current_time - self.last_pong_timestamp > 2 * self.HEARTBEAT_INTERVAL_SECONDS + ): self.close_connection() def connect(self): + """ + Establish the WebSocket connection. + """ headers = { "Authorization": self.auth_token, "x-api-key": self.api_key, "x-client-code": self.client_code, "x-feed-token": self.feed_token } + try: - self.wsapp = websocket.WebSocketApp(self.WEBSOCKET_URI, header=headers, on_open=self.on_open, - on_error=self.on_error, on_close=self.on_close, - on_data=self.on_data, on_ping=self.on_ping, on_pong=self.on_pong) - self.wsapp.run_forever(sslopt={"cert_reqs": ssl.CERT_NONE}, ping_interval=self.HEARTBEAT_INTERVAL_SECONDS) + self.wsapp = WebSocketApp( + self.WEBSOCKET_URI, + header=headers, + on_open=self.on_open, + on_error=self.on_error, + on_close=self.on_close, + on_data=self.on_data, + on_ping=self.on_ping, + on_pong=self.on_pong + ) + + self.wsapp.run_forever( + sslopt={"cert_reqs": ssl.CERT_NONE}, + ping_interval=self.HEARTBEAT_INTERVAL_SECONDS + ) except Exception as e: logger.error("Error connecting to WebSocket: %s", e) self.retry_connect() def retry_connect(self): + """ + Attempt to reconnect on connection failure. + """ if self.current_retry_attempt < self.MAX_CONNECTION_RETRY_ATTEMPTS: - logger.info("Retrying connection (Attempt %s)...", self.current_retry_attempt + 1) + logger.info( + "Retrying connection (Attempt %s)...", + self.current_retry_attempt + 1 + ) time.sleep(self.RETRY_DELAY_SECONDS) self.current_retry_attempt += 1 self.connect() @@ -89,5 +170,8 @@ def retry_connect(self): logger.warning("Max retry attempts reached.") def close_connection(self): + """ + Close the WebSocket connection. + """ if self.wsapp: self.wsapp.close() diff --git a/SmartApi/smartWebSocketV2.py b/SmartApi/smartWebSocketV2.py index 4461b785..1c0966f1 100644 --- a/SmartApi/smartWebSocketV2.py +++ b/SmartApi/smartWebSocketV2.py @@ -73,16 +73,37 @@ def __init__( """ Initialize the SmartWebSocketV2 instance. + This constructor sets up the WebSocket client with the necessary + credentials and retry configuration for establishing and maintaining + a connection with the Smart API's real-time data feed. + Parameters ---------- auth_token : str - JWT auth token received from the Login API. + JWT authentication token received from the Smart API login endpoint. api_key : str - API key from the Smart API account. + Unique API key provided for accessing the Smart API. client_code : str - Angel One account ID. + Your Angel One client code (e.g., "D12345"). feed_token : str - Feed token received from the Login API. + Token required for subscribing to live market feeds. + max_retry_attempt : int, optional + Maximum number of reconnection attempts on failure (default is 1). + retry_strategy : int, optional + Strategy to use for retrying connections: + - 0: Fixed delay + - 1: Exponential backoff (default is 0). + retry_delay : int, optional + Base delay in seconds between retries (default is 10). + retry_multiplier : int, optional + Multiplier applied to delay in case of exponential backoff (default is 2). + retry_duration : int, optional + Total duration (in seconds) to attempt retries before giving up (default is 60). + + Returns + ------- + None + This constructor does not return a value. """ self.auth_token = auth_token @@ -236,7 +257,11 @@ def subscribe( if mode == 4: for token in token_list: if token.get('exchangeType') != 1: - error_message = f"Invalid ExchangeType:{token.get('exchangeType')} Please check the exchange type and try again it support only 1 exchange type" + error_message = ( + f"Invalid ExchangeType: {token.get('exchangeType')}. " + "Please check the exchange type and try again. " + "It supports only one exchange type." + ) logger.error(error_message) raise ValueError(error_message) @@ -253,7 +278,10 @@ def subscribe( total_tokens = sum(len(token["tokens"]) for token in token_list) quota_limit = 50 if total_tokens > quota_limit: - error_message = f"Quota exceeded: You can subscribe to a maximum of {quota_limit} tokens only." + error_message = ( + f"Quota exceeded: You can subscribe to a maximum of {quota_limit} " + "tokens only." + ) logger.error(error_message) raise Exception(error_message) @@ -507,8 +535,8 @@ def _unpack_data( byte_format: str = "I" ) -> tuple: """ - Unpack Binary Data to the integer according to the specified byte_format. - This function returns the tuple + Unpack Binary Data to the integer according to the specified byte_format. + This function returns the tuple """ return struct.unpack(self.LITTLE_ENDIAN_BYTE_ORDER + byte_format, binary_data[start:end]) @@ -586,36 +614,57 @@ def _parse_depth_20_buy_and_sell_data(self, binary_data: bytes) -> dict: def on_message(self, wsapp: WebSocketApp, message: dict) -> None: """ - Handle incoming text messages from the WebSocket. + Handle incoming text messages from the WebSocket server. + + Parameters: + wsapp (WebSocketApp): The WebSocket application instance. + message (dict): The message payload received from the server. """ pass def on_data(self, wsapp: WebSocketApp, data: dict) -> None: """ - Handle incoming binary data from the WebSocket. + Handle incoming binary data from the WebSocket server. + + Parameters: + wsapp (WebSocketApp): The WebSocket application instance. + data (dict): The binary data payload received from the server. """ pass def on_control_message(self, wsapp: WebSocketApp, message: dict) -> None: """ - Handle WebSocket control messages like ping/pong. + Handle WebSocket control messages such as ping, pong, or close frames. + + Parameters: + wsapp (WebSocketApp): The WebSocket application instance. + message (dict): The control message received. """ pass def on_close(self, wsapp: WebSocketApp) -> None: """ Handle WebSocket connection closure. + + Parameters: + wsapp (WebSocketApp): The WebSocket application instance. """ pass def on_open(self, wsapp: WebSocketApp) -> None: """ Handle WebSocket connection open event. + + Parameters: + wsapp (WebSocketApp): The WebSocket application instance. """ pass def on_error(self, error: Exception) -> None: """ - Handle WebSocket errors. + Handle any exceptions or errors raised during the WebSocket connection. + + Parameters: + error (Exception): The exception instance containing error details. """ pass From b453c2007e9f98e7f30ce635bd878cbed774888c Mon Sep 17 00:00:00 2001 From: dhanu Date: Sat, 6 Sep 2025 14:30:29 +0530 Subject: [PATCH 07/16] added login example --- example/example_login.ipynb | 168 ++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 example/example_login.ipynb diff --git a/example/example_login.ipynb b/example/example_login.ipynb new file mode 100644 index 00000000..474ccc71 --- /dev/null +++ b/example/example_login.ipynb @@ -0,0 +1,168 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cc489c91", + "metadata": {}, + "source": [ + "### Importing Logging and SmartConnect" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43dd5072", + "metadata": {}, + "outputs": [], + "source": [ + "from SmartApi import SmartConnect\n", + "from SmartApi.loggerConfig import get_logger \n", + "\n", + "logger = get_logger(__name__, \"INFO\")" + ] + }, + { + "cell_type": "markdown", + "id": "e5db129b", + "metadata": {}, + "source": [ + "#### Define client info" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "55ee2f45", + "metadata": {}, + "outputs": [], + "source": [ + "client_info = {\n", + " \"api_key\": \"\",\n", + " \"client_id\": \"\",\n", + " \"password\": \"\",\n", + " \"totp_secret\": \"\",\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "af40d72d", + "metadata": {}, + "source": [ + "### User (Login Services)" + ] + }, + { + "cell_type": "markdown", + "id": "0e772128", + "metadata": {}, + "source": [ + "##### [SmartAPI User Documentation](https://smartapi.angelbroking.com/docs/User)" + ] + }, + { + "cell_type": "markdown", + "id": "b7fd30f5", + "metadata": {}, + "source": [ + "#### Generate SmartApi session" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ea989d57", + "metadata": {}, + "outputs": [], + "source": [ + "import pyotp\n", + "\n", + "try:\n", + " totp = pyotp.TOTP(client_info[\"totp_secret\"]).now()\n", + "except Exception as e:\n", + " logger.error(\"Invalid Token: The provided token is not valid.\")\n", + " raise e\n", + "\n", + "api = SmartConnect(api_key=client_info[\"api_key\"])\n", + "\n", + "response = api.generateSession(client_info[\"client_id\"], client_info[\"password\"], totp)\n", + "response" + ] + }, + { + "cell_type": "markdown", + "id": "23f1a53d", + "metadata": {}, + "source": [ + "#### Get Profile" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5671aed3", + "metadata": {}, + "outputs": [], + "source": [ + "api.getProfile(refreshToken=api.getrefreshToken)" + ] + }, + { + "cell_type": "markdown", + "id": "bf93c0f4", + "metadata": {}, + "source": [ + "#### Funds and Margins" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20e64040", + "metadata": {}, + "outputs": [], + "source": [ + "api.rmsLimit()" + ] + }, + { + "cell_type": "markdown", + "id": "8e44bc3b", + "metadata": {}, + "source": [ + "#### Logout" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "74a25b82", + "metadata": {}, + "outputs": [], + "source": [ + "api.terminateSession(clientCode=client_info[\"client_id\"])" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "smartapi", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 199cfe58d54f83694eab83f9488ca2e1d55ee347 Mon Sep 17 00:00:00 2001 From: dhanu Date: Sat, 6 Sep 2025 14:30:45 +0530 Subject: [PATCH 08/16] added historical data example --- example/example_historical_data.ipynb | 410 ++++++++++++++++++++++++++ 1 file changed, 410 insertions(+) create mode 100644 example/example_historical_data.ipynb diff --git a/example/example_historical_data.ipynb b/example/example_historical_data.ipynb new file mode 100644 index 00000000..834c79ad --- /dev/null +++ b/example/example_historical_data.ipynb @@ -0,0 +1,410 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cc489c91", + "metadata": {}, + "source": [ + "### Importing Logging and SmartConnect" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "43dd5072", + "metadata": {}, + "outputs": [], + "source": [ + "from SmartApi import SmartConnect\n", + "from SmartApi.loggerConfig import get_logger \n", + "\n", + "logger = get_logger(__name__, \"INFO\")" + ] + }, + { + "cell_type": "markdown", + "id": "e5db129b", + "metadata": {}, + "source": [ + "#### Define client info" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55ee2f45", + "metadata": {}, + "outputs": [], + "source": [ + "client_info = {\n", + " \"api_key\": \"\",\n", + " \"client_id\": \"\",\n", + " \"password\": \"\",\n", + " \"totp_secret\": \"\",\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "b7fd30f5", + "metadata": {}, + "source": [ + "#### Generate SmartApi session" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ea989d57", + "metadata": {}, + "outputs": [], + "source": [ + "import pyotp\n", + "\n", + "try:\n", + " totp = pyotp.TOTP(client_info[\"totp_secret\"]).now()\n", + "except Exception as e:\n", + " logger.error(\"Invalid Token: The provided token is not valid.\")\n", + " raise e\n", + "\n", + "api = SmartConnect(api_key=client_info[\"api_key\"])\n", + "\n", + "response = api.generateSession(client_info[\"client_id\"], client_info[\"password\"], totp)\n", + "response" + ] + }, + { + "cell_type": "markdown", + "id": "8e44bc3b", + "metadata": {}, + "source": [ + "### Historical Data" + ] + }, + { + "cell_type": "markdown", + "id": "f710c4e1", + "metadata": {}, + "source": [ + "##### [SmartAPI Historical API Documentation](https://smartapi.angelbroking.com/docs/Historical)" + ] + }, + { + "cell_type": "markdown", + "id": "486b2f9d", + "metadata": {}, + "source": [ + "#### Fetch Candle Data" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "23c995e9", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-09-06 12:27:42,833 - __main__ - INFO - Historical Candle Data fetched successfully\n" + ] + } + ], + "source": [ + "try:\n", + " exchange = \"NSE\"\n", + " symbol_token = \"3045\"\n", + " interval = \"ONE_MINUTE\" # or \"FIVE_MINUTE\" ect\n", + " fromdate_str = \"2025-08-10 09:15\"\n", + " todate_str = \"2025-09-10 15:15\"\n", + " \n", + " candleParams = {\n", + " \"exchange\": exchange,\n", + " \"symboltoken\": symbol_token,\n", + " \"interval\": interval,\n", + " \"fromdate\": fromdate_str,\n", + " \"todate\": todate_str\n", + " }\n", + " candledetails = api.getCandleData(candleParams)\n", + " if candledetails[\"status\"]:\n", + " candle_data = candledetails[\"data\"]\n", + " logger.info(\"Historical Candle Data fetched successfully\")\n", + " \n", + "except Exception as e:\n", + " logger.exception(f\"Failed to fetch candle data: {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "69f2835a", + "metadata": {}, + "source": [ + "#### Convert Candle Data to DataFrame" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "61f75b6f", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-09-06 12:27:44,586 - __main__ - INFO - Converted candle data to DataFrame\n" + ] + } + ], + "source": [ + "import pandas as pd\n", + "\n", + "DEFAULT_DATAFRAME_COLUMNS = [\"datetime\", \"open\", \"high\", \"low\", \"close\", \"volume\"]\n", + "\n", + "try:\n", + " if candle_data:\n", + " candle_df = pd.DataFrame(candle_data, columns=DEFAULT_DATAFRAME_COLUMNS)\n", + " candle_df['datetime'] = pd.to_datetime(candle_df['datetime'])\n", + " logger.info(\"Converted candle data to DataFrame\")\n", + " \n", + " else:\n", + " candle_df = pd.DataFrame()\n", + " logger.warning(\"No candle data to convert\")\n", + " \n", + "except Exception as e:\n", + " logger.exception(f\"Failed to convert candle data: {e}\")\n", + " candle_df = pd.DataFrame()" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "2d60ba17", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
datetimeopenhighlowclosevolume
02025-08-11 09:15:00+05:30808.00812.40808.00811.45298663
12025-08-11 09:16:00+05:30811.45813.80811.35812.95304812
22025-08-11 09:17:00+05:30813.10815.80812.00815.70303040
32025-08-11 09:18:00+05:30815.70816.80815.25816.50165810
42025-08-11 09:19:00+05:30816.20816.90814.55816.00131647
.....................
67892025-09-06 12:23:00+05:30810.70820.00810.00814.0021713
67902025-09-06 12:24:00+05:30815.00828.35812.10826.05142693
67912025-09-06 12:25:00+05:30826.05827.10812.00821.1038552
67922025-09-06 12:26:00+05:30820.10821.10815.00820.1043140
67932025-09-06 12:27:00+05:30820.85821.00819.70820.1031630
\n", + "

6794 rows × 6 columns

\n", + "
" + ], + "text/plain": [ + " datetime open high low close volume\n", + "0 2025-08-11 09:15:00+05:30 808.00 812.40 808.00 811.45 298663\n", + "1 2025-08-11 09:16:00+05:30 811.45 813.80 811.35 812.95 304812\n", + "2 2025-08-11 09:17:00+05:30 813.10 815.80 812.00 815.70 303040\n", + "3 2025-08-11 09:18:00+05:30 815.70 816.80 815.25 816.50 165810\n", + "4 2025-08-11 09:19:00+05:30 816.20 816.90 814.55 816.00 131647\n", + "... ... ... ... ... ... ...\n", + "6789 2025-09-06 12:23:00+05:30 810.70 820.00 810.00 814.00 21713\n", + "6790 2025-09-06 12:24:00+05:30 815.00 828.35 812.10 826.05 142693\n", + "6791 2025-09-06 12:25:00+05:30 826.05 827.10 812.00 821.10 38552\n", + "6792 2025-09-06 12:26:00+05:30 820.10 821.10 815.00 820.10 43140\n", + "6793 2025-09-06 12:27:00+05:30 820.85 821.00 819.70 820.10 31630\n", + "\n", + "[6794 rows x 6 columns]" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "candle_df" + ] + }, + { + "cell_type": "markdown", + "id": "61bc03f8", + "metadata": {}, + "source": [ + "#### Save DataFrame to CSV" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "a4a79a70", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-09-06 12:31:16,605 - __main__ - INFO - Candle data saved to CSV file: candle_data.csv\n" + ] + } + ], + "source": [ + "try:\n", + " if not candle_df.empty:\n", + " csv_filename = \"candle_data.csv\"\n", + " candle_df.to_csv(csv_filename, index=False)\n", + " logger.info(f\"Candle data saved to CSV file: {csv_filename}\")\n", + " else:\n", + " logger.warning(\"DataFrame is empty, nothing to save\")\n", + "except Exception as e:\n", + " logger.exception(f\"Failed to save candle data to CSV: {e}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17d5c8a0", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "smartapi", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From c43d12d2b0ba15b762526240563fe6886a146890 Mon Sep 17 00:00:00 2001 From: dhanu Date: Sat, 6 Sep 2025 14:30:58 +0530 Subject: [PATCH 09/16] added market data example --- example/example_market_data.ipynb | 225 ++++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 example/example_market_data.ipynb diff --git a/example/example_market_data.ipynb b/example/example_market_data.ipynb new file mode 100644 index 00000000..61a0938a --- /dev/null +++ b/example/example_market_data.ipynb @@ -0,0 +1,225 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cc489c91", + "metadata": {}, + "source": [ + "### Importing Logging and SmartConnect" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "43dd5072", + "metadata": {}, + "outputs": [], + "source": [ + "from SmartApi import SmartConnect\n", + "from SmartApi.loggerConfig import get_logger \n", + "\n", + "logger = get_logger(__name__, \"INFO\")" + ] + }, + { + "cell_type": "markdown", + "id": "e5db129b", + "metadata": {}, + "source": [ + "#### Define client info" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55ee2f45", + "metadata": {}, + "outputs": [], + "source": [ + "client_info = {\n", + " \"api_key\": \"\",\n", + " \"client_id\": \"\",\n", + " \"password\": \"\",\n", + " \"totp_secret\": \"\",\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "b7fd30f5", + "metadata": {}, + "source": [ + "#### Generate SmartApi session" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ea989d57", + "metadata": {}, + "outputs": [], + "source": [ + "import pyotp\n", + "\n", + "try:\n", + " totp = pyotp.TOTP(client_info[\"totp_secret\"]).now()\n", + "except Exception as e:\n", + " logger.error(\"Invalid Token: The provided token is not valid.\")\n", + " raise e\n", + "\n", + "api = SmartConnect(api_key=client_info[\"api_key\"])\n", + "\n", + "response = api.generateSession(client_info[\"client_id\"], client_info[\"password\"], totp)\n", + "response" + ] + }, + { + "cell_type": "markdown", + "id": "19fc7666", + "metadata": {}, + "source": [ + "### Live Market Data" + ] + }, + { + "cell_type": "markdown", + "id": "9fdc950d", + "metadata": {}, + "source": [ + "##### [SmartAPI Market Data Documentation](https://smartapi.angelbroking.com/docs/MarketData)" + ] + }, + { + "cell_type": "markdown", + "id": "cc9e5676", + "metadata": {}, + "source": [ + "#### Get LTP Data" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "4aacfbb5", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-09-06 12:29:29,250 - __main__ - INFO - LTP Data : {'exchange': 'NSE', 'tradingsymbol': 'SBIN-EQ', 'symboltoken': '3045', 'open': 811.5, 'high': 812.5, 'low': 803.65, 'close': 809.4, 'ltp': 806.6}\n" + ] + } + ], + "source": [ + "try:\n", + " exchange = \"NSE\" # or \"BSE\"\n", + " symbol = \"SBIN-EQ\"\n", + " symbol_token = \"3045\"\n", + " \n", + " response = api.ltpData(exchange, symbol, symbol_token)\n", + " ltp = response[\"data\"]\n", + " logger.info(f\"LTP Data : {ltp}\")\n", + "except Exception as e:\n", + " logger.exception(f\"Failed to fetch LTP data: {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "00f30028", + "metadata": {}, + "source": [ + "#### Get Market Data" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "311d501a", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-09-06 12:30:03,159 - __main__ - INFO - Market Data : [{'exchange': 'NSE', 'tradingSymbol': 'SBIN-EQ', 'symbolToken': '3045', 'ltp': 806.6, 'open': 811.5, 'high': 812.5, 'low': 803.65, 'close': 809.4, 'lastTradeQty': 1, 'exchFeedTime': '06-Sep-2025 06:55:05', 'exchTradeTime': '05-Sep-2025 15:56:59', 'netChange': -2.8, 'percentChange': -0.35, 'avgPrice': 807.28, 'tradeVolume': 5194842, 'opnInterest': 162807000, 'lowerCircuit': 725.95, 'upperCircuit': 887.25, 'totBuyQuan': 1021, 'totSellQuan': 0, '52WeekLow': 680.0, '52WeekHigh': 875.45, 'depth': {'buy': [{'price': 806.6, 'quantity': 1021, 'orders': 15}, {'price': 0.0, 'quantity': 0, 'orders': 0}, {'price': 0.0, 'quantity': 0, 'orders': 0}, {'price': 0.0, 'quantity': 0, 'orders': 0}, {'price': 0.0, 'quantity': 0, 'orders': 0}], 'sell': [{'price': 0.0, 'quantity': 0, 'orders': 0}, {'price': 0.0, 'quantity': 0, 'orders': 0}, {'price': 0.0, 'quantity': 0, 'orders': 0}, {'price': 0.0, 'quantity': 0, 'orders': 0}, {'price': 0.0, 'quantity': 0, 'orders': 0}]}}]\n" + ] + } + ], + "source": [ + "try:\n", + " mode = \"FULL\" # or \"OHLC\" or \"LTP\"\n", + " exchangeTokens = {\n", + " \"NSE\": [\"3045\"]\n", + " }\n", + " response = api.getMarketData(mode, exchangeTokens)\n", + " marketData = response[\"data\"][\"fetched\"]\n", + " logger.info(f\"Market Data : {marketData}\")\n", + "except Exception as e:\n", + " logger.exception(f\"Failed to fetch market data: {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "1c2b129e", + "metadata": {}, + "source": [ + "#### Search Scrip" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "6a9d48be", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-09-06 12:30:30,308 - __main__ - INFO - Search Scrip Data : [{'exchange': 'BSE', 'tradingsymbol': 'TITAN', 'symboltoken': '500114'}, {'exchange': 'BSE', 'tradingsymbol': 'TITANBIO', 'symboltoken': '524717'}, {'exchange': 'BSE', 'tradingsymbol': 'TITANIN', 'symboltoken': '521005'}, {'exchange': 'BSE', 'tradingsymbol': 'TITANSEC', 'symboltoken': '530045'}]\n" + ] + } + ], + "source": [ + "try:\n", + " exchange = \"BSE\"\n", + " searchscrip = \"Titan\"\n", + " response = api.searchScrip(exchange, searchscrip)\n", + " searchScripData = response[\"data\"]\n", + " logger.info(f\"Search Scrip Data : {searchScripData}\")\n", + "except Exception as e:\n", + " logger.exception(f\"Failed to search scrip: {e}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1f46ca19", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "smartapi", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From db31ad74d014f456a412cc00260b340d963a14ef Mon Sep 17 00:00:00 2001 From: dhanu Date: Sat, 6 Sep 2025 14:31:13 +0530 Subject: [PATCH 10/16] added ordes example --- example/example_order.ipynb | 503 ++++++++++++++++++++++++++++++++++++ 1 file changed, 503 insertions(+) create mode 100644 example/example_order.ipynb diff --git a/example/example_order.ipynb b/example/example_order.ipynb new file mode 100644 index 00000000..da2025a2 --- /dev/null +++ b/example/example_order.ipynb @@ -0,0 +1,503 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cc489c91", + "metadata": {}, + "source": [ + "### Importing Logging and SmartConnect" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43dd5072", + "metadata": {}, + "outputs": [], + "source": [ + "from SmartApi import SmartConnect\n", + "from SmartApi.loggerConfig import get_logger \n", + "\n", + "logger = get_logger(__name__, \"INFO\")" + ] + }, + { + "cell_type": "markdown", + "id": "e5db129b", + "metadata": {}, + "source": [ + "#### Define client info" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55ee2f45", + "metadata": {}, + "outputs": [], + "source": [ + "client_info = {\n", + " \"api_key\": \"\",\n", + " \"client_id\": \"\",\n", + " \"password\": \"\",\n", + " \"totp_secret\": \"\",\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "b7fd30f5", + "metadata": {}, + "source": [ + "#### Generate SmartApi session" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ea989d57", + "metadata": {}, + "outputs": [], + "source": [ + "import pyotp\n", + "\n", + "try:\n", + " totp = pyotp.TOTP(client_info[\"totp_secret\"]).now()\n", + "except Exception as e:\n", + " logger.error(\"Invalid Token: The provided token is not valid.\")\n", + " raise e\n", + "\n", + "api = SmartConnect(api_key=client_info[\"api_key\"])\n", + "\n", + "response = api.generateSession(client_info[\"client_id\"], client_info[\"password\"], totp)\n", + "response" + ] + }, + { + "cell_type": "markdown", + "id": "feb23c83", + "metadata": {}, + "source": [ + "## Orders" + ] + }, + { + "cell_type": "markdown", + "id": "5bbf2a3d", + "metadata": {}, + "source": [ + "##### [SmartAPI Orders Documentation](https://smartapi.angelbroking.com/docs/Orders)" + ] + }, + { + "cell_type": "markdown", + "id": "5a6f0f52", + "metadata": {}, + "source": [ + "#### Place Orders" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eefdc933", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " orderparams = {\n", + " \"variety\": \"NORMAL\",\n", + " \"tradingsymbol\": \"SBIN-EQ\",\n", + " \"symboltoken\": \"3045\",\n", + " \"transactiontype\": \"BUY\",\n", + " \"exchange\": \"NSE\",\n", + " \"ordertype\": \"LIMIT\",\n", + " \"producttype\": \"INTRADAY\",\n", + " \"duration\": \"DAY\",\n", + " \"price\": \"19500\",\n", + " \"squareoff\": \"0\",\n", + " \"stoploss\": \"0\",\n", + " \"quantity\": \"1\"\n", + " }\n", + " \n", + " # Method 1: Place an order and return the order ID\n", + " orderid = api.placeOrder(orderparams)\n", + " logger.info(f\"PlaceOrder : {orderid}\")\n", + " \n", + " # Method 2: Place an order and return the full response\n", + " response = api.placeOrderFullResponse(orderparams)\n", + " logger.info(f\"PlaceOrder : {response}\")\n", + " \n", + "except Exception as e:\n", + " logger.exception(f\"Order placement failed: {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "0c44ca0e", + "metadata": {}, + "source": [ + "#### Modify an existing Order" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bbf059f3", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " modify_order_params = {\n", + " \"orderid\": orderid, \n", + " \"variety\": \"NORMAL\",\n", + " \"tradingsymbol\": \"SBIN-EQ\",\n", + " \"symboltoken\": \"3045\",\n", + " \"transactiontype\": \"BUY\",\n", + " \"exchange\": \"NSE\",\n", + " \"ordertype\": \"LIMIT\",\n", + " \"producttype\": \"INTRADAY\",\n", + " \"duration\": \"DAY\",\n", + " \"price\": 19600, \n", + " \"quantity\": 2\n", + " }\n", + "\n", + " modified_order_response = api.modifyOrder(modify_order_params)\n", + " logger.info(f\"Order modified successfully: {modified_order_response}\")\n", + "\n", + "except Exception as e:\n", + " logger.exception(f\"Order modification failed: {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "f841b0ea", + "metadata": {}, + "source": [ + "#### Cancel Order" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3e31ebc6", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " cancel_response = api.cancelOrder(orderid, \"NORMAL\")\n", + " logger.info(f\"Cancel Order Response : {cancel_response}\")\n", + "except Exception as e:\n", + " logger.exception(f\"Order cancellation failed: {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "96d74a2d", + "metadata": {}, + "source": [ + "#### Get Order Book" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65260ac2", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " orderbook = api.orderBook()\n", + " logger.info(f\"Order Book: {orderbook}\")\n", + "except Exception as e:\n", + " logger.exception(f\"Failed to fetch order book: {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "614469dc", + "metadata": {}, + "source": [ + "#### Get Trade Book" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fd41b727", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " tradebook = api.tradeBook()\n", + " logger.info(f\"Trade Book : {tradebook}\")\n", + "except Exception as e:\n", + " logger.exception(f\"Failed to fetch trade book: {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "53160d8b", + "metadata": {}, + "source": [ + "#### Get Position" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "54358671", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " pos = api.position()\n", + " logger.info(f\"Position : {pos}\")\n", + "except Exception as e:\n", + " logger.exception(f\"Failed to fetch position: {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "2b80c3c6", + "metadata": {}, + "source": [ + "#### Get Holdings" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2f851151", + "metadata": {}, + "outputs": [], + "source": [ + "# Get Holdings\n", + "try:\n", + " holdings = api.holding()\n", + " logger.info(f\"Holdings : {holdings}\")\n", + "except Exception as e:\n", + " logger.exception(f\"Failed to fetch holdings: {e}\")\n", + "\n", + "# Get All Holdings\n", + "try:\n", + " allholdings = api.allholding()\n", + " logger.info(f\"All Holdings : {allholdings}\")\n", + "except Exception as e:\n", + " logger.exception(f\"Failed to fetch all holdings: {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "c9d1da17", + "metadata": {}, + "source": [ + "#### Convert Position" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "73305682", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " convertparams = {\n", + " \"exchange\": \"NSE\",\n", + " \"oldproducttype\": \"DELIVERY\",\n", + " \"newproducttype\": \"MARGIN\",\n", + " \"tradingsymbol\": \"SBIN-EQ\",\n", + " \"transactiontype\": \"BUY\",\n", + " \"quantity\": 1,\n", + " \"type\": \"DAY\"\n", + " }\n", + " convert_position_response = api.convertPosition(convertparams)\n", + " logger.info(f\"Convert Position Response : {convert_position_response}\")\n", + "except Exception as e:\n", + " logger.exception(f\"Failed to convert position: {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "b886522a", + "metadata": {}, + "source": [ + "#### GTT Create Rule" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0516d39f", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " gttCreateParams = {\n", + " \"tradingsymbol\": \"SBIN-EQ\",\n", + " \"symboltoken\": \"3045\",\n", + " \"exchange\": \"NSE\",\n", + " \"producttype\": \"MARGIN\",\n", + " \"transactiontype\": \"BUY\",\n", + " \"price\": 100000,\n", + " \"qty\": 10,\n", + " \"disclosedqty\": 10,\n", + " \"triggerprice\": 200000,\n", + " \"timeperiod\": 365\n", + " }\n", + " rule_id = api.gttCreateRule(gttCreateParams)\n", + " logger.info(f\"GTT Rule Created: {rule_id}\")\n", + "except Exception as e:\n", + " logger.exception(f\"GTT rule creation failed: {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "4e7e4a02", + "metadata": {}, + "source": [ + "#### GTT Modify Rule" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3a603ddf", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " gttModifyParams = {\n", + " \"id\": rule_id,\n", + " \"symboltoken\": \"3045\",\n", + " \"exchange\": \"NSE\",\n", + " \"price\": 19500,\n", + " \"quantity\": 10,\n", + " \"triggerprice\": 200000,\n", + " \"disclosedqty\": 10,\n", + " \"timeperiod\": 365\n", + " }\n", + " modified_rule_id = api.gttModifyRule(gttModifyParams)\n", + " logger.info(f\"GTT Rule Modified: {modified_rule_id}\")\n", + "except Exception as e:\n", + " logger.exception(f\"GTT rule modification failed: {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "f986dbf4", + "metadata": {}, + "source": [ + "#### GTT Cancel Rule" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "60543e36", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " cancelParams = {\n", + " \"id\": rule_id,\n", + " \"symboltoken\": \"3045\",\n", + " \"exchange\": \"NSE\"\n", + " }\n", + " cancelled_rule_id = api.gttCancelRule(cancelParams)\n", + " logger.info(f\"GTT Rule Cancelled: {cancelled_rule_id}\")\n", + "except Exception as e:\n", + " logger.exception(f\"GTT rule cancellation failed: {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "23b35005", + "metadata": {}, + "source": [ + "#### GTT Details" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "95b4ef16", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " gttdetails = api.gttDetails(rule_id)\n", + " logger.info(f\"GTT Details: {gttdetails}\")\n", + "except Exception as e:\n", + " logger.exception(f\"Failed to fetch GTT details: {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "138a3780", + "metadata": {}, + "source": [ + "#### GTT Lists" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e158d27a", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " gtt_list = api.gttLists('List of status', '', '')\n", + " logger.info(f\"GTT List: {gtt_list}\")\n", + "except Exception as e:\n", + " logger.exception(f\"Failed to fetch GTT list: {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "7a7d722d", + "metadata": {}, + "source": [ + "#### Individual Order Details" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5e3c8041", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " qParam = \"your uniqueorderid\"\n", + " individual_order_details = api.individual_order_details(qParam)\n", + " logger.info(f\"Individual Order Details: {individual_order_details}\")\n", + "except Exception as e:\n", + " logger.exception(f\"Failed to fetch individual order details: {e}\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "smartapi", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From a76e9ed28bcb1e0441740e99a558eda53918c49a Mon Sep 17 00:00:00 2001 From: dhanu Date: Sat, 6 Sep 2025 14:32:27 +0530 Subject: [PATCH 11/16] added websocketV2 example --- example/example_websocketV2.ipynb | 406 ++++++++++++++++++++++++++++++ 1 file changed, 406 insertions(+) create mode 100644 example/example_websocketV2.ipynb diff --git a/example/example_websocketV2.ipynb b/example/example_websocketV2.ipynb new file mode 100644 index 00000000..9f56fb26 --- /dev/null +++ b/example/example_websocketV2.ipynb @@ -0,0 +1,406 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cc489c91", + "metadata": {}, + "source": [ + "### Importing Logging and SmartConnect" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "43dd5072", + "metadata": {}, + "outputs": [], + "source": [ + "from SmartApi import SmartConnect\n", + "from SmartApi.loggerConfig import get_logger \n", + "\n", + "logger = get_logger(__name__, \"INFO\")" + ] + }, + { + "cell_type": "markdown", + "id": "e5db129b", + "metadata": {}, + "source": [ + "#### Define client info" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55ee2f45", + "metadata": {}, + "outputs": [], + "source": [ + "client_info = {\n", + " \"api_key\": \"\",\n", + " \"client_id\": \"\",\n", + " \"password\": \"\",\n", + " \"totp_secret\": \"\",\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "b7fd30f5", + "metadata": {}, + "source": [ + "#### Generate SmartApi session" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ea989d57", + "metadata": {}, + "outputs": [], + "source": [ + "import pyotp\n", + "\n", + "try:\n", + " totp = pyotp.TOTP(client_info[\"totp_secret\"]).now()\n", + "except Exception as e:\n", + " logger.error(\"Invalid Token: The provided token is not valid.\")\n", + " raise e\n", + "\n", + "api = SmartConnect(api_key=client_info[\"api_key\"])\n", + "\n", + "response = api.generateSession(client_info[\"client_id\"], client_info[\"password\"], totp)\n", + "response" + ] + }, + { + "cell_type": "markdown", + "id": "e034b473", + "metadata": {}, + "source": [ + "### WebSocket Streaming" + ] + }, + { + "cell_type": "markdown", + "id": "28d9f908", + "metadata": {}, + "source": [ + "##### [SmartAPI WebSocket Streaming 2.0 Documentation](https://smartapi.angelbroking.com/docs/WebSocket2)" + ] + }, + { + "cell_type": "markdown", + "id": "5033c8dd", + "metadata": {}, + "source": [ + "#### Packages to Import" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "4069452b", + "metadata": {}, + "outputs": [], + "source": [ + "import threading\n", + "from collections import deque\n", + "from SmartApi import SmartWebSocketV2" + ] + }, + { + "cell_type": "markdown", + "id": "357ca244", + "metadata": {}, + "source": [ + "#### Initialize the smartapi Websocket" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dc2fc54b", + "metadata": {}, + "outputs": [], + "source": [ + "sws = SmartWebSocketV2(\n", + " api_key=client_info[\"api_key\"],\n", + " auth_token = api.getaccessToken,\n", + " client_code = client_info[\"client_id\"],\n", + " feed_token = api.getfeedToken\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "44160717", + "metadata": {}, + "source": [ + "##### This line initializes a thread-safe, fixed-size message cache using a deque (with a maximum of 500 messages) and a Lock to ensure safe access from multiple threads." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3dbc0405", + "metadata": {}, + "outputs": [], + "source": [ + "# === Message cache ===\n", + "MAX_CACHE_SIZE = 500\n", + "\n", + "cache = deque(maxlen=MAX_CACHE_SIZE)\n", + "cache_lock = threading.Lock()" + ] + }, + { + "cell_type": "markdown", + "id": "c90513f0", + "metadata": {}, + "source": [ + "#### Define tokens for symbols and mode (customized)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bbaaf2a3", + "metadata": {}, + "outputs": [], + "source": [ + "correlation_id = \"abc123\"\n", + "mode = 2 # 1 (LTP), 2 (Quote), 3 (Snap Quote)\n", + "\n", + "symbols_data = [\n", + " {\n", + " \"name\": \"NIFTY\",\n", + " \"token\": \"99926000\"\n", + " },\n", + " {\n", + " \"name\": \"BANKNIFTY\",\n", + " \"token\": \"99926009\"\n", + " },\n", + "]\n", + "\n", + "token_list = [\n", + " {\n", + " \"exchangeType\": 1,\n", + " \"tokens\": [symbol[\"token\"] for symbol in symbols_data]\n", + " }\n", + "]" + ] + }, + { + "cell_type": "markdown", + "id": "268b1aa8", + "metadata": {}, + "source": [ + "#### Sets up a WebSocket client with logging, event handling, and safe message caching." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b7284fc3", + "metadata": {}, + "outputs": [], + "source": [ + "def on_data(wsapp, message: dict):\n", + " logger.info(f\"Received message: {message}\")\n", + " with cache_lock:\n", + " cache.append(message)\n", + "\n", + "def on_open(wsapp):\n", + " logger.info(\"WebSocket connection opened.\")\n", + " sws.subscribe(correlation_id, mode, token_list)\n", + " logger.info(f\"Subscribed to tokens: {token_list}\")\n", + "\n", + "def on_error(wsapp, error):\n", + " logger.error(f\"WebSocket error: {error}\")\n", + "\n", + "def on_close(wsapp):\n", + " logger.info(\"WebSocket connection closed.\")\n", + "\n", + "\n", + "# === Close WebSocket ===\n", + "def close_ws():\n", + " logger.info(\"Closing WebSocket connection...\")\n", + " sws.close_connection()\n", + "\n", + "# === Graceful Shutdown on Ctrl+C ===\n", + "def handle_exit(sig, frame):\n", + " logger.info(\"SIGINT (Ctrl+C) detected. Exiting...\")\n", + " close_ws()\n", + " exit(0)\n", + "\n", + "\n", + "# === Connect WebSocket ===\n", + "def connect():\n", + " logger.info(\"Attempting to connect to WebSocket...\")\n", + " \n", + " # Assign callbacks\n", + " sws.on_open = on_open\n", + " sws.on_data = on_data\n", + " sws.on_error = on_error\n", + " sws.on_close = on_close\n", + "\n", + " sws.connect()\n", + " logger.info(\"WebSocket connection initiated.\")\n" + ] + }, + { + "cell_type": "markdown", + "id": "1ffe184b", + "metadata": {}, + "source": [ + "#### Starts the connect function in a background thread as a daemon, allowing the main program to continue running while the WebSocket runs in parallel." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "44e6af72", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-09-06 14:13:52,306 - __main__ - INFO - WebSocket opened.\n", + "2025-09-06 14:13:52,308 - __main__ - INFO - Subscribed to: [{'exchangeType': 1, 'tokens': ['99926000', '99926009']}]\n" + ] + } + ], + "source": [ + "threading.Thread(target=connect, daemon=True).start()" + ] + }, + { + "cell_type": "markdown", + "id": "9d3b0b9e", + "metadata": {}, + "source": [ + "### You can use the cached data for processing, analysis, or storage\n" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "4d12bb69", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "deque([{'subscription_mode': 2,\n", + " 'exchange_type': 1,\n", + " 'token': '99926000',\n", + " 'sequence_number': 0,\n", + " 'exchange_timestamp': 1757068691000,\n", + " 'last_traded_price': 2474100,\n", + " 'subscription_mode_val': 'QUOTE',\n", + " 'last_traded_quantity': 0,\n", + " 'average_traded_price': 0,\n", + " 'volume_trade_for_the_day': 0,\n", + " 'total_buy_quantity': 0.0,\n", + " 'total_sell_quantity': 0.0,\n", + " 'open_price_of_the_day': 2481885,\n", + " 'high_price_of_the_day': 2483235,\n", + " 'low_price_of_the_day': 2462160,\n", + " 'closed_price': 2473430},\n", + " {'subscription_mode': 2,\n", + " 'exchange_type': 1,\n", + " 'token': '99926009',\n", + " 'sequence_number': 0,\n", + " 'exchange_timestamp': 1757068691000,\n", + " 'last_traded_price': 5411455,\n", + " 'subscription_mode_val': 'QUOTE',\n", + " 'last_traded_quantity': 0,\n", + " 'average_traded_price': 0,\n", + " 'volume_trade_for_the_day': 0,\n", + " 'total_buy_quantity': 0.0,\n", + " 'total_sell_quantity': 0.0,\n", + " 'open_price_of_the_day': 5430805,\n", + " 'high_price_of_the_day': 5430805,\n", + " 'low_price_of_the_day': 5371955,\n", + " 'closed_price': 5407545}],\n", + " maxlen=500)" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "cache" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "d42ed32d", + "metadata": {}, + "outputs": [], + "source": [ + "def get_symbol_name(token: str):\n", + " for data in symbols_data:\n", + " if data[\"token\"] == token:\n", + " return data[\"name\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "3fcc3c9e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "NIFTY: LTP - 24741.0, Closed Price - 24734.3\n", + "BANKNIFTY: LTP - 54114.55, Closed Price - 54075.45\n" + ] + } + ], + "source": [ + "for item in cache:\n", + " print(\n", + " f\"{get_symbol_name(item['token'])}: \"\n", + " f\"LTP - {item['last_traded_price']/100}, Closed Price - {item['closed_price']/100}\"\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f52eb2fc", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "smartapi", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.13" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} From 132963b734e95fb325ce984ee3fc314c377e6b27 Mon Sep 17 00:00:00 2001 From: dhanu Date: Sat, 6 Sep 2025 14:32:47 +0530 Subject: [PATCH 12/16] removed old examples --- example/sample.py | 186 ----------------------------- example/smartwebsocketexample.py | 38 ------ example/smartwebsocketexamplev2.py | 57 --------- 3 files changed, 281 deletions(-) delete mode 100644 example/sample.py delete mode 100644 example/smartwebsocketexample.py delete mode 100644 example/smartwebsocketexamplev2.py diff --git a/example/sample.py b/example/sample.py deleted file mode 100644 index fa96607f..00000000 --- a/example/sample.py +++ /dev/null @@ -1,186 +0,0 @@ -# package import statement -from SmartApi import SmartConnect #or from smartapi.smartConnect import SmartConnect - -#import smartapi.smartExceptions(for smartExceptions) - -#create object of call -obj=SmartConnect(api_key="your api key") - -#login api call - -data = obj.generateSession("Your Client ID","Your Password","Your totp") - -refreshToken= data['data']['refreshToken'] - -#fetch the feedtoken -feedToken=obj.getfeedToken() - -#fetch User Profile -userProfile= obj.getProfile(refreshToken) -#place order -try: - orderparams = { - "variety": "NORMAL", - "tradingsymbol": "SBIN-EQ", - "symboltoken": "3045", - "transactiontype": "BUY", - "exchange": "NSE", - "ordertype": "LIMIT", - "producttype": "INTRADAY", - "duration": "DAY", - "price": "19500", - "squareoff": "0", - "stoploss": "0", - "quantity": "1" - } - orderId=obj.placeOrder(orderparams) - print("The order id is: {}".format(orderId)) -except Exception as e: - print("Order placement failed: {}".format(e.message)) -#gtt rule creation -try: - gttCreateParams={ - "tradingsymbol" : "SBIN-EQ", - "symboltoken" : "3045", - "exchange" : "NSE", - "producttype" : "MARGIN", - "transactiontype" : "BUY", - "price" : 100000, - "qty" : 10, - "disclosedqty": 10, - "triggerprice" : 200000, - "timeperiod" : 365 - } - rule_id=obj.gttCreateRule(gttCreateParams) - print("The GTT rule id is: {}".format(rule_id)) -except Exception as e: - print("GTT Rule creation failed: {}".format(e.message)) - -#gtt rule list -try: - status=["FORALL"] #should be a list - page=1 - count=10 - lists=obj.gttLists(status,page,count) -except Exception as e: - print("GTT Rule List failed: {}".format(e.message)) - -#Historic api -try: - historicParam={ - "exchange": "NSE", - "symboltoken": "3045", - "interval": "ONE_MINUTE", - "fromdate": "2021-02-08 09:00", - "todate": "2021-02-08 09:16" - } - obj.getCandleData(historicParam) -except Exception as e: - print("Historic Api failed: {}".format(e.message)) -#logout -try: - logout=obj.terminateSession('Your Client Id') - print("Logout Successfull") -except Exception as e: - print("Logout failed: {}".format(e.message)) - -##Estimate Charges -# params = { -# "orders": [ -# { -# "product_type": "DELIVERY", -# "transaction_type": "BUY", -# "quantity": "10", -# "price": "800", -# "exchange": "NSE", -# "symbol_name": "745AS33", -# "token": "17117" -# }, -# # { -# # "product_type": "DELIVERY", -# # "transaction_type": "BUY", -# # "quantity": "10", -# # "price": "800", -# # "exchange": "BSE", -# # "symbol_name": "PIICL151223", -# # "token": "726131" -# # } -# ] -# } -# estimateCharges = obj.estimateCharges(params) -# print(estimateCharges); - -# params = { -# "isin":"INE528G01035", -# "quantity":"1" -# } -# verifyDis = obj.verifyDis(params) -# print(verifyDis); - -# params = { -# "dpId":"33200", -# "ReqId":"2351614738654050", -# "boid":"1203320018563571", -# "pan":"JZTPS2255C" -# } -# generateTPIN = obj.generateTPIN(params) -# print(generateTPIN); - -# params = { -# "ReqId":"2351614738654050" -# } -# getTranStatus = obj.getTranStatus(params) -# print(getTranStatus); - -# params = { -# "name":"TCS", -# "expirydate":"25JAN2024" -# } -# optionGreek = obj.optionGreek(params) -# print(optionGreek); - -# params = { -# "datatype":"PercOIGainers", -# "expirytype":"NEAR" -# } -# gainersLosers = obj.gainersLosers(params) -# print(gainersLosers); - -# putCallRatio = obj.putCallRatio() -# print(putCallRatio); - -# params = { -# "expirytype":"NEAR", -# "datatype":"Long Built Up" -# } -# OIBuildup = obj.oIBuildup(params) -# print(OIBuildup); - -## WebSocket - -from SmartApi.webSocket import WebSocket - -FEED_TOKEN= "your feed token" -CLIENT_CODE="your client Id" -token="channel you want the information of" #"nse_cm|2885&nse_cm|1594&nse_cm|11536" -task="task" #"mw"|"sfi"|"dp" -ss = WebSocket(FEED_TOKEN, CLIENT_CODE) - -def on_tick(ws, tick): - print("Ticks: {}".format(tick)) - -def on_connect(ws, response): - ws.websocket_connection() # Websocket connection - ws.send_request(token,task) - -def on_close(ws, code, reason): - ws.stop() - -# Assign the callbacks. -ss.on_ticks = on_tick -ss.on_connect = on_connect -ss.on_close = on_close - -ss.connect() - - diff --git a/example/smartwebsocketexample.py b/example/smartwebsocketexample.py deleted file mode 100644 index fe620c47..00000000 --- a/example/smartwebsocketexample.py +++ /dev/null @@ -1,38 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Wed May 26 18:50:44 2021 - -@author: Sandip.Khairnar -""" - -from smartapiwebsocket import SmartWebSocket - -# feed_token=092017047 -FEED_TOKEN="092017047" -CLIENT_CODE="S212741" -# token="mcx_fo|224395" -token="nse_cm|2885&nse_cm|1594&nse_cm|11536&nse_cm|3045" -# token="mcx_fo|226745&mcx_fo|220822&mcx_fo|227182&mcx_fo|221599" -task="mw" -ss = SmartWebSocket(FEED_TOKEN, CLIENT_CODE) - -def on_message(ws, message): - print("Ticks: {}".format(message)) - -def on_open(ws): - print("on open") - ss.subscribe(task,token) - -def on_error(ws, error): - print(error) - -def on_close(ws): - print("Close") - -# Assign the callbacks. -ss._on_open = on_open -ss._on_message = on_message -ss._on_error = on_error -ss._on_close = on_close - -ss.connect() \ No newline at end of file diff --git a/example/smartwebsocketexamplev2.py b/example/smartwebsocketexamplev2.py deleted file mode 100644 index a5518b9f..00000000 --- a/example/smartwebsocketexamplev2.py +++ /dev/null @@ -1,57 +0,0 @@ -from SmartApi.smartWebSocketV2 import SmartWebSocketV2 -from logzero import logger - -AUTH_TOKEN = "authToken" -API_KEY = "api_key" -CLIENT_CODE = "client code" -FEED_TOKEN = "feedToken" -correlation_id = "abc123" -action = 1 -mode = 1 - -token_list = [ - { - "exchangeType": 1, - "tokens": ["26009"] - } -] -token_list1 = [ - { - "action": 0, - "exchangeType": 1, - "tokens": ["26009"] - } -] - -sws = SmartWebSocketV2(AUTH_TOKEN, API_KEY, CLIENT_CODE, FEED_TOKEN) - -def on_data(wsapp, message): - logger.info("Ticks: {}".format(message)) - # close_connection() - -def on_open(wsapp): - logger.info("on open") - sws.subscribe(correlation_id, mode, token_list) - # sws.unsubscribe(correlation_id, mode, token_list1) - - -def on_error(wsapp, error): - logger.error(error) - - -def on_close(wsapp): - logger.info("Close") - - - -def close_connection(): - sws.close_connection() - - -# Assign the callbacks. -sws.on_open = on_open -sws.on_data = on_data -sws.on_error = on_error -sws.on_close = on_close - -sws.connect() From eed8f1a6df35c6101b03484172903a299d0bd0af Mon Sep 17 00:00:00 2001 From: dhanu Date: Sat, 6 Sep 2025 15:14:54 +0530 Subject: [PATCH 13/16] updated readme file --- README.md | 400 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 220 insertions(+), 180 deletions(-) diff --git a/README.md b/README.md index 0bf1b15a..d17551e8 100644 --- a/README.md +++ b/README.md @@ -1,213 +1,253 @@ -# SMARTAPI-PYTHON +![SmartAPI Logo](https://smartapi.angelbroking.com/static/media/Smartapi-Logo-1200x212-Transparent.ef58ca94.png) -SMARTAPI-PYTHON is a Python library for interacting with Angel's Trading platform ,that is a set of REST-like HTTP APIs that expose many capabilities required to build stock market investment and trading platforms. It lets you execute orders in real time.. +# SmartAPI - Python +SmartAPI - Python is a library for interacting with Angel Broking's trading platform. It provides REST-like HTTP APIs for building stock market investment and trading applications, including real-time order execution. + +--- ## Installation -Use the package manager [pip](https://pip.pypa.io/en/stable/) to install smartapi-python. +Use [pip](https://pip.pypa.io/en/stable/) to install: + +```bash +pip install smartapi-python +```` +Install additional dependencies: ```bash -pip install -r requirements_dev.txt # for downloading the other required packages +pip install -r requirements_dev.txt # other dependencies ``` -Download the following packages ```bash -pip install pyotp -pip install logzero -pip install websocket-client +pip install pyotp logzero websocket-client ``` -For Downloading pycryptodome package + +For `pycryptodome` (uninstall `pycrypto` first if installed): + ```bash pip uninstall pycrypto -pip install pycryptodome +pip install pycryptodome ``` +--- + ## Usage +### Generate SmartAPI Session + ```python -# package import statement -from SmartApi import SmartConnect #or from SmartApi.smartConnect import SmartConnect import pyotp -from logzero import logger +from SmartApi import SmartConnect +from SmartApi.loggerConfig import get_logger + +logger = get_logger(__name__, "INFO") + +client_info = { + "api_key": "Your Api Key", + "client_id": "Your client code", + "password": "Your pin", + "totp_secret": "Your QR value", +} -api_key = 'Your Api Key' -username = 'Your client code' -pwd = 'Your pin' -smartApi = SmartConnect(api_key) try: - token = "Your QR value" - totp = pyotp.TOTP(token).now() + # Generate TOTP token from secret + totp = pyotp.TOTP(client_info["totp_secret"]).now() except Exception as e: logger.error("Invalid Token: The provided token is not valid.") raise e -correlation_id = "abcde" -data = smartApi.generateSession(username, pwd, totp) +smartApi = SmartConnect(api_key=client_info["api_key"]) +response = smartApi.generateSession(client_info["client_id"], client_info["password"], totp) -if data['status'] == False: - logger.error(data) - +if response.get('status'): + logger.info("Login successful!") else: - # login api call - # logger.info(f"You Credentials: {data}") - authToken = data['data']['jwtToken'] - refreshToken = data['data']['refreshToken'] - # fetch the feedtoken - feedToken = smartApi.getfeedToken() - # fetch User Profile - res = smartApi.getProfile(refreshToken) - smartApi.generateToken(refreshToken) - res=res['data']['exchanges'] - - #place order - try: - orderparams = { - "variety": "NORMAL", - "tradingsymbol": "SBIN-EQ", - "symboltoken": "3045", - "transactiontype": "BUY", - "exchange": "NSE", - "ordertype": "LIMIT", - "producttype": "INTRADAY", - "duration": "DAY", - "price": "19500", - "squareoff": "0", - "stoploss": "0", - "quantity": "1" - } - # Method 1: Place an order and return the order ID - orderid = smartApi.placeOrder(orderparams) - logger.info(f"PlaceOrder : {orderid}") - # Method 2: Place an order and return the full response - response = smartApi.placeOrderFullResponse(orderparams) - logger.info(f"PlaceOrder : {response}") - except Exception as e: - logger.exception(f"Order placement failed: {e}") - - #gtt rule creation - try: - gttCreateParams={ - "tradingsymbol" : "SBIN-EQ", - "symboltoken" : "3045", - "exchange" : "NSE", - "producttype" : "MARGIN", - "transactiontype" : "BUY", - "price" : 100000, - "qty" : 10, - "disclosedqty": 10, - "triggerprice" : 200000, - "timeperiod" : 365 - } - rule_id=smartApi.gttCreateRule(gttCreateParams) - logger.info(f"The GTT rule id is: {rule_id}") - except Exception as e: - logger.exception(f"GTT Rule creation failed: {e}") - - #gtt rule list - try: - status=["FORALL"] #should be a list - page=1 - count=10 - lists=smartApi.gttLists(status,page,count) - except Exception as e: - logger.exception(f"GTT Rule List failed: {e}") - - #Historic api - try: - historicParam={ + logger.error("Login failed!") + logger.error(response) +``` + +### Get Profile + +```python +profile = smartApi.getProfile(refreshToken=smartApi.getrefreshToken) +logger.info(profile) +``` + +--- + +### Place an Order + +```python +try: + orderparams = { + "variety": "NORMAL", + "tradingsymbol": "SBIN-EQ", + "symboltoken": "3045", + "transactiontype": "BUY", + "exchange": "NSE", + "ordertype": "LIMIT", + "producttype": "INTRADAY", + "duration": "DAY", + "price": "19500", + "squareoff": "0", + "stoploss": "0", + "quantity": "1" + } + orderid = smartApi.placeOrder(orderparams) + logger.info(f"Order placed successfully. Order ID: {orderid}") +except Exception as e: + logger.exception(f"Order placement failed: {e}") +``` + +--- + +### GTT Rules + +```python +# Create GTT Rule +try: + gttCreateParams = { + "tradingsymbol": "SBIN-EQ", + "symboltoken": "3045", + "exchange": "NSE", + "producttype": "MARGIN", + "transactiontype": "BUY", + "price": 100000, + "qty": 10, + "disclosedqty": 10, + "triggerprice": 200000, + "timeperiod": 365 + } + rule_id = smartApi.gttCreateRule(gttCreateParams) + logger.info(f"GTT rule created. Rule ID: {rule_id}") +except Exception as e: + logger.exception(f"GTT Rule creation failed: {e}") + +# Fetch GTT Rule List +try: + status = ["FORALL"] + page = 1 + count = 10 + gtt_list = smartApi.gttLists(status, page, count) + logger.info(f"GTT Rules: {gtt_list}") +except Exception as e: + logger.exception(f"GTT Rule List fetch failed: {e}") +``` + +--- + +### Historical Data + +```python +try: + historicParam = { "exchange": "NSE", "symboltoken": "3045", "interval": "ONE_MINUTE", - "fromdate": "2021-02-08 09:00", - "todate": "2021-02-08 09:16" - } - smartApi.getCandleData(historicParam) - except Exception as e: - logger.exception(f"Historic Api failed: {e}") - #logout - try: - logout=smartApi.terminateSession('Your Client Id') - logger.info("Logout Successfull") - except Exception as e: - logger.exception(f"Logout failed: {e}") - - ``` - - ## Getting started with SmartAPI Websocket's - ####### Websocket V2 sample code ####### - - from SmartApi.smartWebSocketV2 import SmartWebSocketV2 - from logzero import logger - - AUTH_TOKEN = "authToken" - API_KEY = "api_key" - CLIENT_CODE = "client code" - FEED_TOKEN = "feedToken" - correlation_id = "abc123" - action = 1 - mode = 1 - - token_list = [ - { - "exchangeType": 1, - "tokens": ["26009"] - } - ] - token_list1 = [ - { - "action": 0, - "exchangeType": 1, - "tokens": ["26009"] - } - ] - - sws = SmartWebSocketV2(AUTH_TOKEN, API_KEY, CLIENT_CODE, FEED_TOKEN) - - def on_data(wsapp, message): - logger.info("Ticks: {}".format(message)) - # close_connection() - - def on_open(wsapp): - logger.info("on open") - sws.subscribe(correlation_id, mode, token_list) - # sws.unsubscribe(correlation_id, mode, token_list1) - - - def on_error(wsapp, error): - logger.error(error) - - - def on_close(wsapp): - logger.info("Close") - - - - def close_connection(): - sws.close_connection() - - - # Assign the callbacks. - sws.on_open = on_open - sws.on_data = on_data - sws.on_error = on_error - sws.on_close = on_close - - sws.connect() - ####### Websocket V2 sample code ENDS Here ####### - - ########################### SmartWebSocket OrderUpdate Sample Code Start Here ########################### - from SmartApi.smartWebSocketOrderUpdate import SmartWebSocketOrderUpdate - client = SmartWebSocketOrderUpdate(AUTH_TOKEN, API_KEY, CLIENT_CODE, FEED_TOKEN) - client.connect() - ########################### SmartWebSocket OrderUpdate Sample Code End Here ########################### + "fromdate": "2025-08-08 09:15", + "todate": "2025-09-08 15:15" + } + candle_data = smartApi.getCandleData(historicParam) + logger.info(candle_data) +except Exception as e: + logger.exception(f"Historic API failed: {e}") +``` + +--- + +### Logout + +```python +try: + logout = smartApi.terminateSession(client_info["client_id"]) + logger.info("Logout Successful") +except Exception as e: + logger.exception(f"Logout failed: {e}") +``` + +--- + +## Getting Started with SmartAPI WebSocket + +### WebSocket V2 Sample + +```python +from SmartApi.smartWebSocketV2 import SmartWebSocketV2 +from SmartApi.loggerConfig import get_logger + +logger = get_logger(__name__, "INFO") + +AUTH_TOKEN = smartApi.getaccessToken +API_KEY = client_info["api_key"] +CLIENT_CODE = client_info["client_id"] +FEED_TOKEN = smartApi.getfeedToken + +correlation_id = "abc123" +action = 1 +mode = 1 + +token_list = [ + { + "exchangeType": 1, + "tokens": ["99926009", "99926000"] + } +] + +sws = SmartWebSocketV2(AUTH_TOKEN, API_KEY, CLIENT_CODE, FEED_TOKEN) + +def on_data(wsapp, message): + logger.info(f"Ticks: {message}") + +def on_open(wsapp): + logger.info("WebSocket opened") + sws.subscribe(correlation_id, mode, token_list) + +def on_error(wsapp, error): + logger.error(error) + +def on_close(wsapp): + logger.info("WebSocket closed") + +sws.on_open = on_open +sws.on_data = on_data +sws.on_error = on_error +sws.on_close = on_close + +sws.connect() ``` -##Change-log -##1.4.5 -- Upgraded TLS Version -##1.4.7 -- Added Error log file +--- + +### SmartWebSocket OrderUpdate Sample + +```python +from SmartApi.smartWebSocketOrderUpdate import SmartWebSocketOrderUpdate + +client = SmartWebSocketOrderUpdate(AUTH_TOKEN, API_KEY, CLIENT_CODE, FEED_TOKEN) +client.connect() +``` + +--- + +## Examples Folder + +Check the `examples/` folder for ready-to-run scripts: + +* `example_login.ipynb` — Generate SmartAPI session with login and TOTP +* `example_order.ipynb` — Place buy/sell orders and manage GTT rules +* `example_historical_data.ipynb` — Fetch historic candle data +* `example_market_data.ipynb` — Fetch live market data +* `example_websocketV2.ipynb` — Connect and subscribe to SmartAPI WebSocket V2 + +--- + +## Notes + +* You need a valid API key and TOTP secret from Angel Broking's developer console. +* Wrap all API calls in try/except blocks to handle errors gracefully. +* Replace placeholders like `"Your Api Key"` and `"Your client code"` with your actual credentials. + +--- -##1.4.8 -- Intgrated EDIS, Brokerage Calculator, Option Greek, TopGainersLosers, PutRatio API \ No newline at end of file +Happy Trading! 🚀 From 2625a9cd866e6607ece898d5d9accfacddaadbb4 Mon Sep 17 00:00:00 2001 From: dhanu Date: Sat, 6 Sep 2025 15:45:26 +0530 Subject: [PATCH 14/16] fixed readme --- README.md | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index d17551e8..1576c140 100644 --- a/README.md +++ b/README.md @@ -6,24 +6,47 @@ SmartAPI - Python is a library for interacting with Angel Broking's trading plat --- -## Installation +## 🔧 Installation -Use [pip](https://pip.pypa.io/en/stable/) to install: +Use [pip](https://pip.pypa.io/en/stable/) to install the latest release: ```bash pip install smartapi-python -```` +``` + +--- + +## 🛠️ Dependency Setup + +If you want to work with the latest code: + +### 1. Clone the repository -Install additional dependencies: ```bash -pip install -r requirements_dev.txt # other dependencies +git clone https://github.com/angel-one/smartapi-python.git +cd smartapi-python ``` +### 2. Install dependencies + ```bash -pip install pyotp logzero websocket-client +pip install -r requirements_dev.txt ``` -For `pycryptodome` (uninstall `pycrypto` first if installed): +--- + +## ⚡ Quick One-Liner (Optional) + +To install development dependencies directly without cloning the repo: + +```bash +pip install -r https://raw.githubusercontent.com/angel-one/smartapi-python/main/requirements_dev.txt + +``` + +--- + +For cryptographic support, install `pycryptodome` (make sure to uninstall `pycrypto` first if it's installed): ```bash pip uninstall pycrypto From a1dcb2deedd5530c9524ee12674803f27da0c3fa Mon Sep 17 00:00:00 2001 From: dhanu Date: Sat, 6 Sep 2025 18:52:49 +0530 Subject: [PATCH 15/16] removed __pycache__ --- SmartApi/__pycache__/__init__.cpython-36.pyc | Bin 122 -> 0 bytes .../__pycache__/smartConnect.cpython-36.pyc | Bin 10666 -> 0 bytes .../__pycache__/smartExceptions.cpython-36.pyc | Bin 3677 -> 0 bytes SmartApi/__pycache__/smartSocket.cpython-36.pyc | Bin 8456 -> 0 bytes SmartApi/__pycache__/socket.cpython-36.pyc | Bin 141 -> 0 bytes SmartApi/__pycache__/socketTP.cpython-36.pyc | Bin 8916 -> 0 bytes SmartApi/__pycache__/version.cpython-36.pyc | Bin 480 -> 0 bytes 7 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 SmartApi/__pycache__/__init__.cpython-36.pyc delete mode 100644 SmartApi/__pycache__/smartConnect.cpython-36.pyc delete mode 100644 SmartApi/__pycache__/smartExceptions.cpython-36.pyc delete mode 100644 SmartApi/__pycache__/smartSocket.cpython-36.pyc delete mode 100644 SmartApi/__pycache__/socket.cpython-36.pyc delete mode 100644 SmartApi/__pycache__/socketTP.cpython-36.pyc delete mode 100644 SmartApi/__pycache__/version.cpython-36.pyc diff --git a/SmartApi/__pycache__/__init__.cpython-36.pyc b/SmartApi/__pycache__/__init__.cpython-36.pyc deleted file mode 100644 index f619741a91256bb1d9bdccb6de0f3f4660282320..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 122 zcmXr!<>i|2e04kr0|UcjAcg}*Aj<)Wi&=m~3PUi1CZpdlIYq;;_lhPbtkwwF4PY48#lox{Dl* diff --git a/SmartApi/__pycache__/smartConnect.cpython-36.pyc b/SmartApi/__pycache__/smartConnect.cpython-36.pyc deleted file mode 100644 index 7a7be233af39fa670bae0a86f2d1555df0d14b7d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10666 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 W<$E>hBHaI_5N2?+O2F%x(*GBkYsT^b diff --git a/SmartApi/__pycache__/smartExceptions.cpython-36.pyc b/SmartApi/__pycache__/smartExceptions.cpython-36.pyc deleted file mode 100644 index 0abc8e087c8daca464e62acd1f3d0e6a089aeb77..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3677 zcmcha&u`pB6vxLu_E(ZkHWXS?3K|dxER|KD=>b$BDvGK|G^q#|Ru$I7Gn=^ej?Ih> zB-&dd^@_w_!9T%?zrqJDoO0oWK;p!E<6W;eNvU>M!I39^Gvo1_?`Pi3+dJ*{(ri^OtHL&6ESPT>~h#OYaJ+uQ-$5f<1s*eZQhd?B07|XLiEkXjvj6n$gAK&^W7%54u zAvv+1+C%HavdGXL65rt#jAkEiY|AhWSri@%njQ)&#)EcuJU6fDED=h<@FZ{geh7Vi zKX-$e3(v`2Euwzj7znM|flxK*l_k)OO?fXBIf4ZN2{6wR_ED7IFj?%+eJ}LZ9 z!lI#^h3+Ot3Z+2jwb8`xSjRUnOt(9B#Iux*->ji;k_`m-w&eSRm}e0_ulWAA8H zAV8~u%=NtUP)MOzRL=Hz>%OF!G)jg2Q1ILIpy)zD@K|-}1JP$$lu{I%hML~p-0c1b z>FeN1mGsa)u`O8TFQx1#6kM%gffMr_RmbNB76sEn8zYtx2)nJoZ3H5)h|bThil_q} zy}DY);qWEZh9YnHK92)HfMcmuEG~zEeqoDQ5lzdrji(FZSp6z~CgcRh7hj4*2`yx* zDT^YSVl!ouQ0g? ziUtCN?5ZXtJPh_g_>YG zA4)Mc(~e5)+~x=TP9gL!Si(`iZTskLTn#FpgIcpNcxCLNW@H-?H%b<4Cs5ikM_Ka$JE`;54&f7T^TRTI%nav zg-VWiU7r%6it7#NH771~=mY#ni3@8M#MRjm>G!dEHX-(OikK{#A$QZ6p!;A6Oe&Jl zgcT;+bJK$)HK_u86FSZb5PkXxzf%IlnjNo|w@Ud3Y%@->>p-Pnz@yUW|4AYN}D(D6+at30a=O0e6-W7{{0KcvjBDg diff --git a/SmartApi/__pycache__/smartSocket.cpython-36.pyc b/SmartApi/__pycache__/smartSocket.cpython-36.pyc deleted file mode 100644 index 99c5ce76ec693eecd9d0c7dd42322946387804f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8456 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 Kj6j{mATt5^8X|)L diff --git a/SmartApi/__pycache__/socketTP.cpython-36.pyc b/SmartApi/__pycache__/socketTP.cpython-36.pyc deleted file mode 100644 index 5b561855ad7a8ef47081986783773cdd55be90ea..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8916 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 diff --git a/SmartApi/__pycache__/version.cpython-36.pyc b/SmartApi/__pycache__/version.cpython-36.pyc deleted file mode 100644 index 5c18fcb3a55deda9eb0f6d3343285d535f761f4c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 480 zcmZ8eOHRWu5Ka1_O{=P1u|p~wL>Eo?NGvKqEv#6;rmU<=mRnn?)g-oThp0IV7vLV8 zB3n)YE5=PD6>QCD^ybaS&)2uZp~UG`RObPSu}i5V#AYgC?rc}k`H-eSg45M zKsi|(BlRQ*vMlq7r7ugizY=kf-&E<4n{~R}frqSdi3k5DH!uAQ|0?g2<+w`a$5rHO zx|xPC;gRiE)SsVTUq)WxVQjdG7{)~(W6JbOacP$XpoMX&B8<@hG9pVNL1>8-U5wwD z(j~8hF-hU=3aGZ9*f3iTRePA(v4B8T#8)iQFz&-q9w@zYKWC4VMYZT&$Hm<03d$|^ YgM7Iy+PATwX~gdBZ|MQJhWq#a0{I1-00000 From 7ed6793ab8be80293452d7af5d58d2babaa28f6a Mon Sep 17 00:00:00 2001 From: dhanu Date: Sun, 7 Sep 2025 07:55:39 +0530 Subject: [PATCH 16/16] added color to logger --- SmartApi/loggerConfig.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/SmartApi/loggerConfig.py b/SmartApi/loggerConfig.py index 6243b1a0..6c951346 100644 --- a/SmartApi/loggerConfig.py +++ b/SmartApi/loggerConfig.py @@ -7,10 +7,28 @@ # Ensure the log directory exists os.makedirs(LOG_DIR, exist_ok=True) +# ANSI color codes +LOG_COLORS = { + 'DEBUG': "\033[32m", # Green + 'INFO': "\033[32m", # Green + 'WARNING': "\033[33m", # Yellow + 'ERROR': "\033[31m", # Red + 'CRITICAL': "\033[41m", # Red background + 'RESET': "\033[0m" # Reset +} + +class ColorFormatter(logging.Formatter): + def format(self, record): + log_color = LOG_COLORS.get(record.levelname, LOG_COLORS['RESET']) + reset = LOG_COLORS['RESET'] + message = super().format(record) + return f"{log_color}{message}{reset}" + + def get_logger(name: str, level: str = "WARNING") -> logging.Logger: """ Creates and returns a logger instance that logs messages at the specified level and above. - Logs are saved in logs/app.log and also printed to console. + Logs are saved in logs/app.log and also printed to console with color. Parameters: name (str): Logger name. @@ -30,13 +48,16 @@ def get_logger(name: str, level: str = "WARNING") -> logging.Logger: file_handler = logging.FileHandler(log_path) file_handler.setLevel(getattr(logging, level.upper(), logging.WARNING)) - # Console handler + # Console handler with color console_handler = logging.StreamHandler() console_handler.setLevel(getattr(logging, level.upper(), logging.WARNING)) + # Formatter formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + color_formatter = ColorFormatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + file_handler.setFormatter(formatter) - console_handler.setFormatter(formatter) + console_handler.setFormatter(color_formatter) logger.addHandler(file_handler) logger.addHandler(console_handler)