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/README.md b/README.md index 0bf1b15a..1576c140 100644 --- a/README.md +++ b/README.md @@ -1,213 +1,276 @@ -# SMARTAPI-PYTHON + -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. +## 🔧 Installation + +Use [pip](https://pip.pypa.io/en/stable/) to install the latest release: ```bash -pip install -r requirements_dev.txt # for downloading the other required packages +pip install smartapi-python ``` -Download the following packages +--- + +## 🛠️ Dependency Setup + +If you want to work with the latest code: + +### 1. Clone the repository + ```bash -pip install pyotp -pip install logzero -pip install websocket-client +git clone https://github.com/angel-one/smartapi-python.git +cd smartapi-python ``` -For Downloading pycryptodome package + +### 2. Install dependencies + +```bash +pip install -r requirements_dev.txt +``` + +--- + +## ⚡ 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 -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}") ``` -##Change-log -##1.4.5 -- Upgraded TLS Version -##1.4.7 -- Added Error log file +--- + +### 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() +``` + +--- + +### 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! 🚀 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/__pycache__/__init__.cpython-36.pyc b/SmartApi/__pycache__/__init__.cpython-36.pyc deleted file mode 100644 index f619741a..00000000 Binary files a/SmartApi/__pycache__/__init__.cpython-36.pyc and /dev/null differ diff --git a/SmartApi/__pycache__/smartConnect.cpython-36.pyc b/SmartApi/__pycache__/smartConnect.cpython-36.pyc deleted file mode 100644 index 7a7be233..00000000 Binary files a/SmartApi/__pycache__/smartConnect.cpython-36.pyc and /dev/null differ diff --git a/SmartApi/__pycache__/smartExceptions.cpython-36.pyc b/SmartApi/__pycache__/smartExceptions.cpython-36.pyc deleted file mode 100644 index 0abc8e08..00000000 Binary files a/SmartApi/__pycache__/smartExceptions.cpython-36.pyc and /dev/null differ diff --git a/SmartApi/__pycache__/smartSocket.cpython-36.pyc b/SmartApi/__pycache__/smartSocket.cpython-36.pyc deleted file mode 100644 index 99c5ce76..00000000 Binary files a/SmartApi/__pycache__/smartSocket.cpython-36.pyc and /dev/null differ diff --git a/SmartApi/__pycache__/socket.cpython-36.pyc b/SmartApi/__pycache__/socket.cpython-36.pyc deleted file mode 100644 index addd724a..00000000 Binary files a/SmartApi/__pycache__/socket.cpython-36.pyc and /dev/null differ diff --git a/SmartApi/__pycache__/socketTP.cpython-36.pyc b/SmartApi/__pycache__/socketTP.cpython-36.pyc deleted file mode 100644 index 5b561855..00000000 Binary files a/SmartApi/__pycache__/socketTP.cpython-36.pyc and /dev/null differ diff --git a/SmartApi/__pycache__/version.cpython-36.pyc b/SmartApi/__pycache__/version.cpython-36.pyc deleted file mode 100644 index 5c18fcb3..00000000 Binary files a/SmartApi/__pycache__/version.cpython-36.pyc and /dev/null differ diff --git a/SmartApi/loggerConfig.py b/SmartApi/loggerConfig.py new file mode 100644 index 00000000..6c951346 --- /dev/null +++ b/SmartApi/loggerConfig.py @@ -0,0 +1,65 @@ +import logging +import os + +LOG_DIR = "logs" +LOG_FILE = "app.log" + +# 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 with color. + + 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 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(color_formatter) + + logger.addHandler(file_handler) + logger.addHandler(console_handler) + + return logger diff --git a/SmartApi/smartConnect.py b/SmartApi/smartConnect.py index d39fd53e..fd0372f4 100644 --- a/SmartApi/smartConnect.py +++ b/SmartApi/smartConnect.py @@ -1,90 +1,183 @@ -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__ +from SmartApi.loggerConfig import get_logger +import SmartApi.smartExceptions as ex + +logger = get_logger(__name__, level="WARNING") + + +# 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): + """ + 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" + 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, + ): + + """ + 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 @@ -94,25 +187,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 +218,79 @@ 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 +298,552 @@ 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 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 setFeedToken(self, feedToken: str) -> None: + """ + Set the market feed token. + """ + self.feed_token = feedToken + + + 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): - """Alias for sending a DELETE request.""" + 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): - """Alias for sending a PUT request.""" + + + 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): - """Alias for sending a POST request.""" + + + 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): - """Alias for sending a GET request.""" + + + 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__ 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/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 5f4262fa..1c0966f1 100644 --- a/SmartApi/smartWebSocketV2.py +++ b/SmartApi/smartWebSocketV2.py @@ -2,17 +2,24 @@ import time import ssl import json -import websocket -import os -import logging -import logzero -from logzero import logger +from websocket import WebSocketApp + +from SmartApi.loggerConfig import get_logger + +logger = get_logger(__name__, level="WARNING") + 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,54 @@ 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. + + 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 authentication token received from the Smart API login endpoint. + api_key : str + Unique API key provided for accessing the Smart API. + client_code : str + Your Angel One client code (e.g., "D12345"). + feed_token : str + 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 self.api_key = api_key self.client_code = client_code @@ -76,27 +117,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 +145,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 @@ -184,7 +257,11 @@ def subscribe(self, correlation_id, mode, token_list): 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) @@ -201,7 +278,10 @@ def subscribe(self, correlation_id, mode, token_list): 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) @@ -211,8 +291,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 +346,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 +373,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 +386,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 +414,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 +434,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 +446,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 +473,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,10 +527,16 @@ 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 + 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]) @@ -410,19 +549,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 +579,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 +591,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 +611,60 @@ 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 server. + + Parameters: + wsapp (WebSocketApp): The WebSocket application instance. + message (dict): The message payload received from the server. + """ pass - def on_data(self, wsapp, data): + def on_data(self, wsapp: WebSocketApp, data: dict) -> None: + """ + 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, message): + def on_control_message(self, wsapp: WebSocketApp, message: dict) -> None: + """ + 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): + def on_close(self, wsapp: WebSocketApp) -> None: + """ + Handle WebSocket connection closure. + + Parameters: + wsapp (WebSocketApp): The WebSocket application instance. + """ pass - def on_open(self, wsapp): + def on_open(self, wsapp: WebSocketApp) -> None: + """ + Handle WebSocket connection open event. + + Parameters: + wsapp (WebSocketApp): The WebSocket application instance. + """ pass - def on_error(self): + def on_error(self, error: Exception) -> None: + """ + Handle any exceptions or errors raised during the WebSocket connection. + + Parameters: + error (Exception): The exception instance containing error details. + """ pass 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/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", + " | datetime | \n", + "open | \n", + "high | \n", + "low | \n", + "close | \n", + "volume | \n", + "
|---|---|---|---|---|---|---|
| 0 | \n", + "2025-08-11 09:15:00+05:30 | \n", + "808.00 | \n", + "812.40 | \n", + "808.00 | \n", + "811.45 | \n", + "298663 | \n", + "
| 1 | \n", + "2025-08-11 09:16:00+05:30 | \n", + "811.45 | \n", + "813.80 | \n", + "811.35 | \n", + "812.95 | \n", + "304812 | \n", + "
| 2 | \n", + "2025-08-11 09:17:00+05:30 | \n", + "813.10 | \n", + "815.80 | \n", + "812.00 | \n", + "815.70 | \n", + "303040 | \n", + "
| 3 | \n", + "2025-08-11 09:18:00+05:30 | \n", + "815.70 | \n", + "816.80 | \n", + "815.25 | \n", + "816.50 | \n", + "165810 | \n", + "
| 4 | \n", + "2025-08-11 09:19:00+05:30 | \n", + "816.20 | \n", + "816.90 | \n", + "814.55 | \n", + "816.00 | \n", + "131647 | \n", + "
| ... | \n", + "... | \n", + "... | \n", + "... | \n", + "... | \n", + "... | \n", + "... | \n", + "
| 6789 | \n", + "2025-09-06 12:23:00+05:30 | \n", + "810.70 | \n", + "820.00 | \n", + "810.00 | \n", + "814.00 | \n", + "21713 | \n", + "
| 6790 | \n", + "2025-09-06 12:24:00+05:30 | \n", + "815.00 | \n", + "828.35 | \n", + "812.10 | \n", + "826.05 | \n", + "142693 | \n", + "
| 6791 | \n", + "2025-09-06 12:25:00+05:30 | \n", + "826.05 | \n", + "827.10 | \n", + "812.00 | \n", + "821.10 | \n", + "38552 | \n", + "
| 6792 | \n", + "2025-09-06 12:26:00+05:30 | \n", + "820.10 | \n", + "821.10 | \n", + "815.00 | \n", + "820.10 | \n", + "43140 | \n", + "
| 6793 | \n", + "2025-09-06 12:27:00+05:30 | \n", + "820.85 | \n", + "821.00 | \n", + "819.70 | \n", + "820.10 | \n", + "31630 | \n", + "
6794 rows × 6 columns
\n", + "