├── .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 | [](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 |
--------------------------------------------------------------------------------