├── .gitignore ├── LICENSE ├── README.md ├── portfolio.sample.xlsx ├── requirements.txt └── zerodha.py /.gitignore: -------------------------------------------------------------------------------- 1 | cookies.pkl 2 | credentials.json 3 | chromedriver 4 | portfolio.xlsx -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Ali Jafri 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kite-Zerodha Selenium automation ( WIP ) 2 | 3 | > Have your computer manage your protfolio when you work/sleep 4 | 5 | > Kite Zerodha order automation with Entry/Target/StopLoss price by giving in a simple portfolio 6 | 7 | > Ensure that Marketwatch 7 has space left for the stocks in your portfolio 8 | 9 | --- 10 | 11 | ## Additional Files 12 | 13 | > Don't forget to create a `credentials.json` file as below 14 | 15 | ```json 16 | { 17 | "username": "ABC123", 18 | "password": "thisIsMyPassword" 19 | } 20 | ``` 21 | 22 | > Use sample `portfolio.sample.xlsx` and create `portfolio.xlsx` as per your wish. 23 | 24 | --- 25 | 26 | ## Requriements 27 | 28 | - See `requirements.txt` for dependencies 29 | 30 | ## Features 31 | 32 | The idea is to have a Excel/CSV file whereIn your stockName,EntryPrice,Target,SL,Quantity would be mentioned. 33 | This script will be like a daemon/cron-job to regularly check for updates and place buy/sell order automatically without user intervention thereby giving freedom to user to continue working on his/her stuff ( or sleep zzzZZZZ ) 34 | 35 | ## Usage 36 | 37 | - Run the script using `python zerodha.py` 38 | 39 | --- 40 | 41 | ## Support 42 | 43 | Reach out to me at one of the following places! 44 | 45 | - Facebook at `@theAliJafri` 46 | 47 | --- 48 | 49 | ## Donations (Optional) 50 | 51 | - PhonePe: decodeit@ybl 52 | 53 | 54 | --- 55 | 56 | ## License 57 | 58 | [![License](http://img.shields.io/:license-mit-blue.svg?style=flat-square)](http://badges.mit-license.org) 59 | 60 | - **[MIT license](http://opensource.org/licenses/mit-license.php)** 61 | - Copyright 2018 © Ali Jafri. 62 | -------------------------------------------------------------------------------- /portfolio.sample.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deCodeIt/Zerodha-Selenium-Automation/a66ec8fa3df7e2a964d9ffa2a07a6beaf92ab0d1/portfolio.sample.xlsx -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4==4.12.2 2 | certifi==2020.4.5.1 3 | cffi==1.15.1 4 | chardet==3.0.4 5 | charset-normalizer==3.0.1 6 | colorama==0.4.3 7 | configparser==5.0.0 8 | crayons==0.3.0 9 | cryptography==39.0.0 10 | docopt==0.6.2 11 | et-xmlfile==1.1.0 12 | idna==2.9 13 | numpy==1.25.0 14 | openpyxl==3.1.2 15 | pandas==2.0.2 16 | pipreqs==0.4.11 17 | protobuf==3.19.4 18 | pycparser==2.21 19 | PyJWT==2.6.0 20 | pyserial==3.5 21 | python-dateutil==2.8.2 22 | python-dotenv==0.21.1 23 | python-engineio==3.13.2 24 | python-socketio==4.6.1 25 | pytz==2023.3 26 | pywatchman==1.4.1 27 | requests==2.23.0 28 | selenium==3.141.0 29 | six==1.16.0 30 | socketIO-client==0.7.2 31 | soupsieve==2.4.1 32 | termcolor==2.3.0 33 | terminaltables==3.1.10 34 | tzdata==2023.3 35 | urllib3==1.25.9 36 | webdriver-manager==2.3.0 37 | websocket-client==1.5.3 38 | yarg==0.1.9 39 | -------------------------------------------------------------------------------- /zerodha.py: -------------------------------------------------------------------------------- 1 | #!/usr/local/bin/python 2 | 3 | from selenium import webdriver 4 | from selenium.webdriver.support.ui import WebDriverWait 5 | from selenium.common.exceptions import TimeoutException 6 | from selenium.webdriver.common.by import By 7 | from selenium.webdriver.support import expected_conditions as EC 8 | from webdriver_manager.chrome import ChromeDriverManager 9 | from selenium.webdriver.common.action_chains import ActionChains 10 | from selenium.webdriver.remote.webelement import WebElement 11 | from selenium.webdriver.common.keys import Keys 12 | from typing import List 13 | import pandas as pd 14 | from os import listdir 15 | from time import sleep 16 | import json 17 | import pickle 18 | import pdb 19 | 20 | class ZerodhaSelenium( object ): 21 | 22 | def __init__( self ): 23 | self.timeout = 5 24 | self.username : str = None 25 | self.password: str = None 26 | self.loadCredentials() 27 | 28 | def setup( self ): 29 | self.driver = webdriver.Chrome(ChromeDriverManager().install(), options=self.getChromeOptions()) 30 | 31 | def getChromeOptions( self ): 32 | chromeOptions = webdriver.ChromeOptions() 33 | chromeOptions.add_argument( '--disable-notifications' ) 34 | return chromeOptions 35 | 36 | def getCssElement( self, cssSelector: str, timeout: int = None ) -> WebElement: 37 | ''' 38 | To make sure we wait till the element appears 39 | ''' 40 | return WebDriverWait( self.driver, self.timeout if timeout is None else timeout ).until( EC.presence_of_element_located( ( By.CSS_SELECTOR, cssSelector ) ) ) 41 | 42 | def getCssElements( self, cssSelector: str, timeout: int = None ) -> List[WebElement]: 43 | ''' 44 | To make sure we wait till the element appears 45 | ''' 46 | return WebDriverWait( self.driver, self.timeout if timeout is None else timeout ).until( EC.presence_of_all_elements_located( ( By.CSS_SELECTOR, cssSelector ) ) ) 47 | 48 | def waitForCssElement( self, cssSelector: str, timeout: int = None ): 49 | ''' 50 | Wait till the element appears 51 | ''' 52 | WebDriverWait( self.driver, self.timeout if timeout is None else timeout ).until( EC.presence_of_element_located( ( By.CSS_SELECTOR, cssSelector ) ) ) 53 | 54 | def waitForUrl( self, url: str, timeout: int = None ): 55 | ''' 56 | Wait till the element appears 57 | ''' 58 | WebDriverWait( self.driver, self.timeout if timeout is None else timeout ).until( EC.url_contains( url ) ) 59 | 60 | def hoverOverCssElement( self, cssSelector: str, timeout: int = None ): 61 | action = ActionChains( self.driver ) 62 | elem = self.getCssElement( cssSelector, timeout ) 63 | action.move_to_element( elem ) 64 | action.perform() 65 | 66 | def hoverOverElement( self, elem: WebElement ): 67 | action = ActionChains( self.driver ) 68 | action.move_to_element( elem ) 69 | action.perform() 70 | 71 | def saveSession( self ): 72 | with open( 'cookies.pkl', 'wb' ) as file: 73 | pickle.dump( self.driver.get_cookies(), file ) 74 | 75 | def maybeRestoreSession( self ): 76 | if not 'cookies.pkl' in listdir(): 77 | return 78 | 79 | self.driver.get( 'https://kite.zerodha.com/404' ) 80 | 81 | with open('cookies.pkl', 'rb') as file: 82 | cookies = pickle.load( file ) 83 | 84 | for cookie in cookies: 85 | self.driver.add_cookie( cookie ) 86 | 87 | self.driver.get( 'https://kite.zerodha.com/' ) 88 | 89 | def loadCredentials( self ): 90 | '''Load saved login credentials from credentials.json file''' 91 | with open( 'credentials.json' ) as credsFile: 92 | data = json.load( credsFile ) 93 | self.username = data[ 'username' ] 94 | self.password = data[ 'password' ] 95 | 96 | def isLoggedIn( self ): 97 | try: 98 | self.getCssElement( "span.user-id" ) 99 | return True 100 | except TimeoutException: 101 | return False 102 | 103 | def doLogin( self ): 104 | #let's login 105 | self.driver.get( "https://kite.zerodha.com/") 106 | try: 107 | passwordField = self.getCssElement( "input#password" ) 108 | passwordField.send_keys( self.password ) 109 | userNameField = self.getCssElement( "input#userid" ) 110 | userNameField.send_keys( self.username ) 111 | 112 | loginButton = self.getCssElement( "button[type=submit]" ) 113 | loginButton.click() 114 | 115 | # 2FA 116 | self.waitForCssElement( "form.twofa-form" ) 117 | # Wait for user to enter TOTP manually 118 | 119 | self.waitForUrl( "kite.zerodha.com/dashboard", 300 ) 120 | 121 | self.saveSession() 122 | 123 | except TimeoutException: 124 | print( "Timeout occurred" ) 125 | 126 | # pdb.set_trace() 127 | 128 | def openMarketwatch( self ): 129 | '''Opens up marketwatch 7 which should ideally be reserved for this script''' 130 | mws = self.driver.find_elements_by_css_selector( "ul.marketwatch-selector li" ) 131 | mws[ -2 ].click() 132 | 133 | def clearMarketwatch( self ): 134 | try: 135 | while True: 136 | self.hoverOverCssElement( "div.instruments div.instrument", 1 ) 137 | deleteElem = self.getCssElement( "div.instruments div.instrument span[data-balloon^='Delete']" ) 138 | deleteElem.click() 139 | except TimeoutException: 140 | print( "Cleared" ) 141 | 142 | def pasrseExcel( self ): 143 | # Read the Excel file 144 | portfolio = pd.read_excel( 'portfolio.xlsx' ) # Replace 'file_path.xlsx' with the actual file path 145 | 146 | # Access the columns 147 | stockNames = portfolio['StockName'] 148 | entryPrices = portfolio['EntryPrice'] 149 | targetPrices = portfolio['TargetPrice'] 150 | stopLosses = portfolio['StopLoss'] 151 | quantities = portfolio['Quantity'] 152 | 153 | for i in range( len( portfolio ) ): 154 | stockName = stockNames[i] 155 | entryPrice = entryPrices[i] 156 | targetPrice = targetPrices[i] 157 | stopLoss = stopLosses[i] 158 | quantity = quantities[i] 159 | 160 | # Do something with the data (e.g., print or perform calculations) 161 | print(f"Stock Name: {stockName}, {entryPrice}, {targetPrice}, {stopLoss}, {quantity}") 162 | 163 | self.addStock( stockName ) 164 | 165 | def addStock( self, stockCode: str ): 166 | nicknameElem = self.getCssElement( "span.nickname" ) 167 | searchElem = self.getCssElement( "input[placeholder^='Search eg']" ) 168 | searchElem.send_keys( stockCode ) 169 | # TODO: replace with proper awaiting of search results 170 | sleep( 1 ) 171 | searchResults = self.getCssElements( "ul.omnisearch-results li" ) 172 | for stock in searchResults: 173 | if stock.find_element_by_css_selector( "span.tradingsymbol" ).text.strip() == stockCode and stock.find_element_by_css_selector( "span.exchange-tag" ).text.strip() == "NSE": 174 | self.hoverOverElement( stock ) 175 | stock.find_element_by_css_selector( "span.action-buttons button[data-balloon='Add']").click() 176 | print( f"Stock {stockCode} added") 177 | # searchElem.send_keys( Keys.BACKSPACE * len( stockCode ) ) 178 | nicknameElem.click() 179 | return 180 | 181 | # searchElem.send_keys( Keys.BACKSPACE * len( stockCode ) ) 182 | nicknameElem.click() 183 | print( f"Stock {stockCode} wasn't found") 184 | 185 | def loadPositions( self ): 186 | self.driver.get( "https://kite.zerodha.com/positions" ) 187 | 188 | positions = self.getCssElements( "div.positions > section.open-positions table tbody tr:not([class='show-all-row'])" ) 189 | if not positions: 190 | return 191 | 192 | for position in positions: 193 | product = position.find_element_by_css_selector( "td.product" ).text.strip() 194 | stockCode = position.find_element_by_css_selector( "td.instrument span.tradingsymbol" ).text.strip() 195 | exchange = position.find_element_by_css_selector( "td.instrument span.exchange" ).text.strip() 196 | qty = int( position.find_element_by_css_selector( "td.quantity" ).text.strip() ) 197 | print( f"Position: {stockCode} {exchange} {qty} {product}" ) 198 | 199 | def close( self ): 200 | self.driver.quit() 201 | 202 | if __name__ == "__main__": 203 | obj = ZerodhaSelenium() 204 | obj.setup() 205 | obj.maybeRestoreSession() 206 | if not obj.isLoggedIn(): 207 | obj.doLogin() 208 | obj.openMarketwatch() 209 | obj.clearMarketwatch() 210 | obj.pasrseExcel() 211 | obj.loadPositions() 212 | # obj.close() 213 | --------------------------------------------------------------------------------