├── .coveragerc ├── .github └── workflows │ ├── test-conda.yml │ └── test.yml ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── dev_requirements.txt ├── examples ├── flask_app.py ├── gtt_order.py ├── order_margins.py ├── simple.py ├── threaded_ticker.py └── ticker.py ├── kiteconnect ├── __init__.py ├── __version__.py ├── connect.py ├── exceptions.py └── ticker.py ├── pytest.ini ├── setup.cfg ├── setup.py └── tests ├── __init__.py ├── helpers ├── __init__.py └── utils.py ├── integration ├── __init__.py ├── conftest.py ├── test_connect_read.py └── test_connect_write.py └── unit ├── __init__.py ├── conftest.py ├── test_connect.py ├── test_exceptions.py ├── test_kite_object.py └── test_ticker.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | source = kiteconnect 4 | 5 | [report] 6 | exclude_lines = 7 | pragma: no cover 8 | ignore_errors = True 9 | omit = 10 | examples/* 11 | tests/* 12 | test*.py 13 | setup.py 14 | -------------------------------------------------------------------------------- /.github/workflows/test-conda.yml: -------------------------------------------------------------------------------- 1 | name: Lint/Test Conda 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | name: (${{ matrix.python-version }}, ${{ matrix.os }}) 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | max-parallel: 3 10 | fail-fast: false 11 | matrix: 12 | os: ["ubuntu-20.04", "windows-latest"] 13 | python-version: [3.5, 3.6, 3.7, 3.8, 3.9, "3.10"] 14 | steps: 15 | - uses: conda-incubator/setup-miniconda@v2 16 | with: 17 | auto-update-conda: true 18 | python-version: ${{ matrix.python-version }} 19 | 20 | - name: Conda info 21 | shell: bash -l {0} 22 | run: conda info 23 | 24 | - uses: actions/checkout@v2 25 | with: 26 | submodules: true 27 | - name: Install dependencies 28 | shell: bash -l {0} 29 | run: | 30 | conda install pip 31 | pip install --upgrade setuptools 32 | pip install -r dev_requirements.txt 33 | pip install -e . 34 | 35 | - name: Lint with flake8 36 | shell: bash -l {0} 37 | run: | 38 | # stop the build if there are Python syntax errors or undefined names 39 | flake8 . --count --show-source --statistics 40 | 41 | - name: Test with pytest 42 | shell: bash -l {0} 43 | run: | 44 | py.test --cov=./ 45 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Lint/Test 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | runs-on: ${{ matrix.os }} 7 | strategy: 8 | max-parallel: 3 9 | fail-fast: false 10 | matrix: 11 | os: [ubuntu-20.04, windows-latest] 12 | python-version: [3.5, 3.6, 3.7, 3.8, 3.9, "3.10"] 13 | steps: 14 | - uses: actions/checkout@v2 15 | with: 16 | submodules: true 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install --upgrade setuptools 26 | pip install --upgrade wheel 27 | pip install -r dev_requirements.txt 28 | pip install -e . 29 | 30 | - name: Lint with flake8 31 | run: | 32 | # stop the build if there are Python syntax errors or undefined names 33 | flake8 . --count --show-source --statistics 34 | 35 | - name: Test with pytest 36 | run: | 37 | py.test --cov=./ 38 | 39 | # Build and publish if its a new tag and use latest Python version to push dists 40 | - name: Build 41 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') && matrix.os == 'ubuntu-20.04' && matrix.python-version == '3.8' 42 | run: | 43 | python setup.py sdist bdist_wheel 44 | 45 | - name: Publish package to TestPyPI 46 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') && matrix.os == 'ubuntu-20.04' && matrix.python-version == '3.8' 47 | uses: pypa/gh-action-pypi-publish@master 48 | with: 49 | verbose: true 50 | user: __token__ 51 | password: ${{ secrets.PYPI_TOKEN }} 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,linux,macos,python,windows 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### macOS ### 20 | *.DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | 24 | # Icon must end with two \r 25 | Icon 26 | 27 | # Thumbnails 28 | ._* 29 | 30 | # Files that might appear in the root of a volume 31 | .DocumentRevisions-V100 32 | .fseventsd 33 | .Spotlight-V100 34 | .TemporaryItems 35 | .Trashes 36 | .VolumeIcon.icns 37 | .com.apple.timemachine.donotpresent 38 | 39 | # Directories potentially created on remote AFP share 40 | .AppleDB 41 | .AppleDesktop 42 | Network Trash Folder 43 | Temporary Items 44 | .apdisk 45 | 46 | ### OSX ### 47 | 48 | # Icon must end with two \r 49 | 50 | # Thumbnails 51 | 52 | # Files that might appear in the root of a volume 53 | 54 | # Directories potentially created on remote AFP share 55 | 56 | ### Python ### 57 | # Byte-compiled / optimized / DLL files 58 | __pycache__/ 59 | *.py[cod] 60 | *$py.class 61 | 62 | # C extensions 63 | *.so 64 | 65 | # Distribution / packaging 66 | .Python 67 | env/ 68 | build/ 69 | develop-eggs/ 70 | dist/ 71 | downloads/ 72 | eggs/ 73 | .eggs/ 74 | lib/ 75 | lib64/ 76 | parts/ 77 | sdist/ 78 | var/ 79 | wheels/ 80 | *.egg-info/ 81 | .installed.cfg 82 | *.egg 83 | 84 | # PyInstaller 85 | # Usually these files are written by a python script from a template 86 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 87 | *.manifest 88 | *.spec 89 | 90 | # Installer logs 91 | pip-log.txt 92 | pip-delete-this-directory.txt 93 | 94 | # Unit test / coverage reports 95 | htmlcov/ 96 | .tox/ 97 | .coverage 98 | .coverage.* 99 | .cache 100 | nosetests.xml 101 | coverage.xml 102 | *,cover 103 | .hypothesis/ 104 | 105 | # Translations 106 | *.mo 107 | *.pot 108 | 109 | # Django stuff: 110 | *.log 111 | local_settings.py 112 | 113 | # Flask stuff: 114 | instance/ 115 | .webassets-cache 116 | 117 | # Scrapy stuff: 118 | .scrapy 119 | 120 | # Sphinx documentation 121 | docs/_build/ 122 | 123 | # PyBuilder 124 | target/ 125 | 126 | # Jupyter Notebook 127 | .ipynb_checkpoints 128 | 129 | # pyenv 130 | .python-version 131 | 132 | # celery beat schedule file 133 | celerybeat-schedule 134 | 135 | # SageMath parsed files 136 | *.sage.py 137 | 138 | # dotenv 139 | .env 140 | 141 | # virtualenv 142 | .venv 143 | venv/ 144 | ENV/ 145 | 146 | # Spyder project settings 147 | .spyderproject 148 | .spyproject 149 | 150 | # Rope project settings 151 | .ropeproject 152 | 153 | # mkdocs documentation 154 | /site 155 | 156 | ### Windows ### 157 | # Windows thumbnail cache files 158 | Thumbs.db 159 | ehthumbs.db 160 | ehthumbs_vista.db 161 | 162 | # Folder config file 163 | Desktop.ini 164 | 165 | # Recycle Bin used on file shares 166 | $RECYCLE.BIN/ 167 | 168 | # Windows Installer files 169 | *.cab 170 | *.msi 171 | *.msm 172 | *.msp 173 | 174 | # Windows shortcuts 175 | *.lnk 176 | 177 | # Vscode 178 | .vscode 179 | 180 | # coverage 181 | cov_html 182 | 183 | # End of https://www.gitignore.io/api/osx,linux,macos,python,windows 184 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tests/mock_responses"] 2 | path = tests/mock_responses 3 | url = https://github.com/zerodha/kiteconnect-mocks 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Zerodha Technology Pvt. Ltd. (India) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Kite Connect API Python client - v4 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/kiteconnect.svg)](https://pypi.python.org/pypi/kiteconnect) 4 | [![Build Status](https://travis-ci.org/zerodhatech/pykiteconnect.svg?branch=kite3)](https://travis-ci.org/zerodhatech/pykiteconnect) 5 | [![Windows Build Status](https://ci.appveyor.com/api/projects/status/github/zerodhatech/pykiteconnect?svg=true)](https://ci.appveyor.com/project/rainmattertech/pykiteconnect) 6 | [![codecov.io](https://codecov.io/gh/zerodhatech/pykiteconnect/branch/kite3/graphs/badge.svg?branch=kite3)](https://codecov.io/gh/zerodhatech/pykiteconnect/branch/kite3) 7 | 8 | The official Python client for communicating with the [Kite Connect API](https://kite.trade). 9 | 10 | Kite Connect is a set of REST-like APIs that expose many capabilities required to build a complete investment and trading platform. Execute orders in real time, manage user portfolio, stream live market data (WebSockets), and more, with the simple HTTP API collection. 11 | 12 | [Zerodha Technology](https://zerodha.com) (c) 2021. Licensed under the MIT License. 13 | 14 | ## Documentation 15 | 16 | - [Python client documentation](https://kite.trade/docs/pykiteconnect/v4) 17 | - [Kite Connect HTTP API documentation](https://kite.trade/docs/connect/v3) 18 | 19 | ## v4 - Breaking changes 20 | 21 | - Renamed ticker fields as per [kite connect doc](https://kite.trade/docs/connect/v3/websocket/#quote-packet-structure) 22 | - Renamed `bsecds` to `bcd` in `ticker.EXCHANGE_MAP` 23 | 24 | ## v5 - Breaking changes 25 | 26 | - **Drop Support for Python 2.7**: Starting from version v5, support for Python 2.7 has been discontinued. This decision was made due to the [announcement](https://github.com/actions/setup-python/issues/672) by `setup-python`, which stopped supporting Python 2.x since May 2023. 27 | 28 | - **For Python 2.x Users**: If you are using Python 2.x, you can continue using the `kiteconnect` library, but please stick to the <= 4.x.x versions of the library. You can find the previous releases on the [PyKiteConnect GitHub Releases](https://github.com/zerodha/pykiteconnect/releases) page. 29 | 30 | ## Installing the client 31 | 32 | You can install the pre release via pip 33 | 34 | ``` 35 | pip install --upgrade kiteconnect 36 | ``` 37 | 38 | Its recommended to update `setuptools` to latest if you are facing any issue while installing 39 | 40 | ``` 41 | pip install -U pip setuptools 42 | ``` 43 | 44 | Since some of the dependencies uses C extensions it has to compiled before installing the package. 45 | 46 | ### Linux, BSD and macOS 47 | 48 | - On Linux, and BSDs, you will need a C compiler (such as GCC). 49 | 50 | #### Debian/Ubuntu 51 | 52 | ``` 53 | apt-get install libffi-dev python-dev python3-dev 54 | ``` 55 | 56 | #### Centos/RHEL/Fedora 57 | 58 | ``` 59 | yum install libffi-devel python3-devel python-devel 60 | ``` 61 | 62 | #### macOS/OSx 63 | 64 | ``` 65 | xcode-select --install 66 | ``` 67 | 68 | ### Microsoft Windows 69 | 70 | Each Python version uses a specific compiler version (e.g. CPython 2.7 uses Visual C++ 9.0, CPython 3.3 uses Visual C++ 10.0, etc). So, you need to install the compiler version that corresponds to your Python version 71 | 72 | - Python 2.6, 2.7, 3.0, 3.1, 3.2 - [Microsoft Visual C++ 9.0](https://wiki.python.org/moin/WindowsCompilers#Microsoft_Visual_C.2B-.2B-_9.0_standalone:_Visual_C.2B-.2B-_Compiler_for_Python_2.7_.28x86.2C_x64.29) 73 | - Python 3.3, 3.4 - [Microsoft Visual C++ 10.0](https://wiki.python.org/moin/WindowsCompilers#Microsoft_Visual_C.2B-.2B-_10.0_standalone:_Windows_SDK_7.1_.28x86.2C_x64.2C_ia64.29) 74 | - Python 3.5, 3.6 - [Microsoft Visual C++ 14.0](https://wiki.python.org/moin/WindowsCompilers#Microsoft_Visual_C.2B-.2B-_14.0_standalone:_Visual_C.2B-.2B-_Build_Tools_2015_.28x86.2C_x64.2C_ARM.29) 75 | 76 | For more details check [official Python documentation](https://wiki.python.org/moin/WindowsCompilers). 77 | 78 | ## API usage 79 | 80 | ```python 81 | import logging 82 | from kiteconnect import KiteConnect 83 | 84 | logging.basicConfig(level=logging.DEBUG) 85 | 86 | kite = KiteConnect(api_key="your_api_key") 87 | 88 | # Redirect the user to the login url obtained 89 | # from kite.login_url(), and receive the request_token 90 | # from the registered redirect url after the login flow. 91 | # Once you have the request_token, obtain the access_token 92 | # as follows. 93 | 94 | data = kite.generate_session("request_token_here", api_secret="your_secret") 95 | kite.set_access_token(data["access_token"]) 96 | 97 | # Place an order 98 | try: 99 | order_id = kite.place_order(tradingsymbol="INFY", 100 | exchange=kite.EXCHANGE_NSE, 101 | transaction_type=kite.TRANSACTION_TYPE_BUY, 102 | quantity=1, 103 | variety=kite.VARIETY_AMO, 104 | order_type=kite.ORDER_TYPE_MARKET, 105 | product=kite.PRODUCT_CNC, 106 | validity=kite.VALIDITY_DAY) 107 | 108 | logging.info("Order placed. ID is: {}".format(order_id)) 109 | except Exception as e: 110 | logging.info("Order placement failed: {}".format(e.message)) 111 | 112 | # Fetch all orders 113 | kite.orders() 114 | 115 | # Get instruments 116 | kite.instruments() 117 | 118 | # Place an mutual fund order 119 | kite.place_mf_order( 120 | tradingsymbol="INF090I01239", 121 | transaction_type=kite.TRANSACTION_TYPE_BUY, 122 | amount=5000, 123 | tag="mytag" 124 | ) 125 | 126 | # Cancel a mutual fund order 127 | kite.cancel_mf_order(order_id="order_id") 128 | 129 | # Get mutual fund instruments 130 | kite.mf_instruments() 131 | ``` 132 | 133 | Refer to the [Python client documentation](https://kite.trade/docs/pykiteconnect/v4) for the complete list of supported methods. 134 | 135 | ## WebSocket usage 136 | 137 | ```python 138 | import logging 139 | from kiteconnect import KiteTicker 140 | 141 | logging.basicConfig(level=logging.DEBUG) 142 | 143 | # Initialise 144 | kws = KiteTicker("your_api_key", "your_access_token") 145 | 146 | def on_ticks(ws, ticks): 147 | # Callback to receive ticks. 148 | logging.debug("Ticks: {}".format(ticks)) 149 | 150 | def on_connect(ws, response): 151 | # Callback on successful connect. 152 | # Subscribe to a list of instrument_tokens (RELIANCE and ACC here). 153 | ws.subscribe([738561, 5633]) 154 | 155 | # Set RELIANCE to tick in `full` mode. 156 | ws.set_mode(ws.MODE_FULL, [738561]) 157 | 158 | def on_close(ws, code, reason): 159 | # On connection close stop the main loop 160 | # Reconnection will not happen after executing `ws.stop()` 161 | ws.stop() 162 | 163 | # Assign the callbacks. 164 | kws.on_ticks = on_ticks 165 | kws.on_connect = on_connect 166 | kws.on_close = on_close 167 | 168 | # Infinite loop on the main thread. Nothing after this will run. 169 | # You have to use the pre-defined callbacks to manage subscriptions. 170 | kws.connect() 171 | ``` 172 | 173 | ## Run unit tests 174 | 175 | ```sh 176 | python setup.py test 177 | ``` 178 | 179 | or 180 | 181 | ```sh 182 | pytest -s tests/unit --cov-report html:cov_html --cov=./ 183 | ``` 184 | 185 | ## Run integration tests 186 | 187 | ```sh 188 | pytest -s tests/integration/ --cov-report html:cov_html --cov=./ --api-key api_key --access-token access_token 189 | ``` 190 | 191 | ## Generate documentation 192 | 193 | ```sh 194 | pip install pdoc 195 | 196 | pdoc --html --html-dir docs kiteconnect 197 | ``` 198 | 199 | ## Changelog 200 | 201 | [Check release notes](https://github.com/zerodha/pykiteconnect/releases) 202 | -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | pytest>=4.6.11 2 | responses>=0.12.1 3 | pytest-cov>=2.10.1 4 | flake8>=3.8.4, <= 4.0.1 5 | mock>=3.0.5 6 | urllib3<2.0 -------------------------------------------------------------------------------- /examples/flask_app.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) Zerodha Technology Pvt. Ltd. 6 | # 7 | # This is simple Flask based webapp to generate access token and get basic 8 | # account info like holdings and order. 9 | # 10 | # To run this you need Kite Connect python client and Flask webserver 11 | # 12 | # pip install Flask 13 | # pip install kiteconnect 14 | # 15 | # python examples/flask_app.py 16 | ############################################################################### 17 | import os 18 | import json 19 | import logging 20 | from datetime import date, datetime 21 | from decimal import Decimal 22 | 23 | from flask import Flask, request, jsonify, session 24 | from kiteconnect import KiteConnect 25 | 26 | logging.basicConfig(level=logging.DEBUG) 27 | 28 | # Base settings 29 | PORT = 5010 30 | HOST = "127.0.0.1" 31 | 32 | 33 | def serializer(obj): return isinstance(obj, (date, datetime, Decimal)) and str(obj) # noqa 34 | 35 | 36 | # Kite Connect App settings. Go to https://developers.kite.trade/apps/ 37 | # to create an app if you don't have one. 38 | kite_api_key = "kite_api_key" 39 | kite_api_secret = "kite_api_secret" 40 | 41 | # Create a redirect url 42 | redirect_url = "http://{host}:{port}/login".format(host=HOST, port=PORT) 43 | 44 | # Login url 45 | login_url = "https://kite.zerodha.com/connect/login?api_key={api_key}".format(api_key=kite_api_key) 46 | 47 | # Kite connect console url 48 | console_url = "https://developers.kite.trade/apps/{api_key}".format(api_key=kite_api_key) 49 | 50 | # App 51 | app = Flask(__name__) 52 | app.secret_key = os.urandom(24) 53 | 54 | # Templates 55 | index_template = """ 56 |
Make sure your app with api_key - {api_key} has set redirect to {redirect_url}.
57 |
If not you can set it from your Kite Connect developer console here.
58 |

Login to generate access token.

""" 59 | 60 | login_template = """ 61 |

Success

62 |
Access token: {access_token}
63 |

User login data

64 |
{user_data}
65 |

Fetch user holdings

66 |

Fetch user orders

67 |

Checks Kite Connect docs for other calls.

""" 68 | 69 | 70 | def get_kite_client(): 71 | """Returns a kite client object 72 | """ 73 | kite = KiteConnect(api_key=kite_api_key) 74 | if "access_token" in session: 75 | kite.set_access_token(session["access_token"]) 76 | return kite 77 | 78 | 79 | @app.route("/") 80 | def index(): 81 | return index_template.format( 82 | api_key=kite_api_key, 83 | redirect_url=redirect_url, 84 | console_url=console_url, 85 | login_url=login_url 86 | ) 87 | 88 | 89 | @app.route("/login") 90 | def login(): 91 | request_token = request.args.get("request_token") 92 | 93 | if not request_token: 94 | return """ 95 | 96 | Error while generating request token. 97 | 98 | Try again.""" 99 | 100 | kite = get_kite_client() 101 | data = kite.generate_session(request_token, api_secret=kite_api_secret) 102 | session["access_token"] = data["access_token"] 103 | 104 | return login_template.format( 105 | access_token=data["access_token"], 106 | user_data=json.dumps( 107 | data, 108 | indent=4, 109 | sort_keys=True, 110 | default=serializer 111 | ) 112 | ) 113 | 114 | 115 | @app.route("/holdings.json") 116 | def holdings(): 117 | kite = get_kite_client() 118 | return jsonify(holdings=kite.holdings()) 119 | 120 | 121 | @app.route("/orders.json") 122 | def orders(): 123 | kite = get_kite_client() 124 | return jsonify(orders=kite.orders()) 125 | 126 | 127 | if __name__ == "__main__": 128 | logging.info("Starting server: http://{host}:{port}".format(host=HOST, port=PORT)) 129 | app.run(host=HOST, port=PORT, debug=True) 130 | -------------------------------------------------------------------------------- /examples/gtt_order.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from kiteconnect import KiteConnect 3 | 4 | logging.basicConfig(level=logging.DEBUG) 5 | 6 | kite = KiteConnect(api_key="your_api_key") 7 | 8 | # Redirect the user to the login url obtained 9 | # from kite.login_url(), and receive the request_token 10 | # from the registered redirect url after the login flow. 11 | # Once you have the request_token, obtain the access_token 12 | # as follows. 13 | 14 | data = kite.generate_session("request_token_here", secret="your_secret") 15 | kite.set_access_token(data["access_token"]) 16 | 17 | # Place single-leg gtt order - https://kite.trade/docs/connect/v3/gtt/#single 18 | try: 19 | order_single = [{ 20 | "exchange":"NSE", 21 | "tradingsymbol": "SBIN", 22 | "transaction_type": kite.TRANSACTION_TYPE_BUY, 23 | "quantity": 1, 24 | "order_type": "LIMIT", 25 | "product": "CNC", 26 | "price": 470, 27 | }] 28 | single_gtt = kite.place_gtt(trigger_type=kite.GTT_TYPE_SINGLE, tradingsymbol="SBIN", exchange="NSE", trigger_values=[470], last_price=473, orders=order_single) 29 | logging.info("single leg gtt order trigger_id : {}".format(single_gtt['trigger_id'])) 30 | except Exception as e: 31 | logging.info("Error placing single leg gtt order: {}".format(e)) 32 | 33 | 34 | # Place two-leg(OCO) gtt order - https://kite.trade/docs/connect/v3/gtt/#two-leg 35 | try: 36 | order_oco = [{ 37 | "exchange":"NSE", 38 | "tradingsymbol": "SBIN", 39 | "transaction_type": kite.TRANSACTION_TYPE_SELL, 40 | "quantity": 1, 41 | "order_type": "LIMIT", 42 | "product": "CNC", 43 | "price": 470 44 | },{ 45 | "exchange":"NSE", 46 | "tradingsymbol": "SBIN", 47 | "transaction_type": kite.TRANSACTION_TYPE_SELL, 48 | "quantity": 1, 49 | "order_type": "LIMIT", 50 | "product": "CNC", 51 | "price": 480 52 | }] 53 | gtt_oco = kite.place_gtt(trigger_type=kite.GTT_TYPE_OCO, tradingsymbol="SBIN", exchange="NSE", trigger_values=[470,480], last_price=473, orders=order_oco) 54 | logging.info("GTT OCO trigger_id : {}".format(gtt_oco['trigger_id'])) 55 | except Exception as e: 56 | logging.info("Error placing gtt oco order: {}".format(e)) -------------------------------------------------------------------------------- /examples/order_margins.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from kiteconnect import KiteConnect 3 | 4 | logging.basicConfig(level=logging.DEBUG) 5 | 6 | kite = KiteConnect(api_key="your_api_key") 7 | 8 | # Redirect the user to the login url obtained 9 | # from kite.login_url(), and receive the request_token 10 | # from the registered redirect url after the login flow. 11 | # Once you have the request_token, obtain the access_token 12 | # as follows. 13 | 14 | data = kite.generate_session("request_token_here", secret="your_secret") 15 | kite.set_access_token(data["access_token"]) 16 | 17 | # Fetch margin detail for order/orders 18 | try: 19 | # Fetch margin detail for single order 20 | order_param_single = [{ 21 | "exchange": "NSE", 22 | "tradingsymbol": "INFY", 23 | "transaction_type": "BUY", 24 | "variety": "regular", 25 | "product": "MIS", 26 | "order_type": "MARKET", 27 | "quantity": 2 28 | }] 29 | 30 | margin_detail = kite.order_margins(order_param_single) 31 | logging.info("Required margin for single order: {}".format(margin_detail)) 32 | 33 | # Fetch margin detail for list of orders 34 | order_param_multi = [{ 35 | "exchange": "NSE", 36 | "tradingsymbol": "SBIN", 37 | "transaction_type": "BUY", 38 | "variety": "regular", 39 | "product": "MIS", 40 | "order_type": "MARKET", 41 | "quantity": 10 42 | }, 43 | { 44 | "exchange": "NFO", 45 | "tradingsymbol": "TCS20DECFUT", 46 | "transaction_type": "BUY", 47 | "variety": "regular", 48 | "product": "MIS", 49 | "order_type": "LIMIT", 50 | "quantity": 5, 51 | "price":2725.30 52 | }, 53 | { 54 | "exchange": "NFO", 55 | "tradingsymbol": "NIFTY20DECFUT", 56 | "transaction_type": "BUY", 57 | "variety": "bo", 58 | "product": "MIS", 59 | "order_type": "MARKET", 60 | "quantity": 5 61 | }] 62 | 63 | margin_detail = kite.order_margins(order_param_multi) 64 | logging.info("Required margin for order_list: {}".format(margin_detail)) 65 | 66 | # Basket orders 67 | order_param_basket = [ 68 | { 69 | "exchange": "NFO", 70 | "tradingsymbol": "NIFTY21JUN15400PE", 71 | "transaction_type": "BUY", 72 | "variety": "regular", 73 | "product": "MIS", 74 | "order_type": "MARKET", 75 | "quantity": 75 76 | }, 77 | { 78 | "exchange": "NFO", 79 | "tradingsymbol": "NIFTY21JUN14450PE", 80 | "transaction_type": "SELL", 81 | "variety": "regular", 82 | "product": "MIS", 83 | "order_type": "MARKET", 84 | "quantity": 150 85 | }] 86 | 87 | margin_amount = kite.basket_order_margins(order_param_basket) 88 | logging.info("Required margin for basket order: {}".format(margin_amount)) 89 | # Compact margin response 90 | margin_amount_comt = kite.basket_order_margins(order_param_basket, mode='compact') 91 | logging.info("Required margin for basket order in compact form: {}".format(margin_amount_comt)) 92 | 93 | except Exception as e: 94 | logging.info("Error fetching order margin: {}".format(e)) 95 | 96 | 97 | # Fetch virtual contract note charges 98 | try: 99 | order_book_params = [ 100 | { 101 | "order_id": "111111111", 102 | "exchange": "NSE", 103 | "tradingsymbol": "SBIN", 104 | "transaction_type": "BUY", 105 | "variety": "regular", 106 | "product": "CNC", 107 | "order_type": "MARKET", 108 | "quantity": 1, 109 | "average_price": 560 110 | }, 111 | { 112 | "order_id": "2222222222", 113 | "exchange": "MCX", 114 | "tradingsymbol": "GOLDPETAL23AUGFUT", 115 | "transaction_type": "SELL", 116 | "variety": "regular", 117 | "product": "NRML", 118 | "order_type": "LIMIT", 119 | "quantity": 1, 120 | "average_price": 5862 121 | }, 122 | { 123 | "order_id": "3333333333", 124 | "exchange": "NFO", 125 | "tradingsymbol": "NIFTY23AUG17900PE", 126 | "transaction_type": "BUY", 127 | "variety": "regular", 128 | "product": "NRML", 129 | "order_type": "LIMIT", 130 | "quantity": 100, 131 | "average_price": 1.5 132 | }] 133 | 134 | order_book_charges = kite.get_virtual_contract_note(order_book_params) 135 | logging.info("Virtual contract note charges: {}".format(order_book_charges)) 136 | except Exception as e: 137 | logging.info("Error fetching virtual contract note charges: {}".format(e)) -------------------------------------------------------------------------------- /examples/simple.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from kiteconnect import KiteConnect 3 | 4 | logging.basicConfig(level=logging.DEBUG) 5 | 6 | kite = KiteConnect(api_key="your_api_key") 7 | 8 | # Redirect the user to the login url obtained 9 | # from kite.login_url(), and receive the request_token 10 | # from the registered redirect url after the login flow. 11 | # Once you have the request_token, obtain the access_token 12 | # as follows. 13 | 14 | data = kite.generate_session("request_token_here", secret="your_secret") 15 | kite.set_access_token(data["access_token"]) 16 | 17 | # Place an order 18 | try: 19 | order_id = kite.place_order( 20 | variety=kite.VARIETY_REGULAR, 21 | exchange=kite.EXCHANGE_NSE, 22 | tradingsymbol="INFY", 23 | transaction_type=kite.TRANSACTION_TYPE_BUY, 24 | quantity=1, 25 | product=kite.PRODUCT_CNC, 26 | order_type=kite.ORDER_TYPE_MARKET 27 | ) 28 | 29 | logging.info("Order placed. ID is: {}".format(order_id)) 30 | except Exception as e: 31 | logging.info("Order placement failed: {}".format(e)) 32 | 33 | # Fetch all orders 34 | kite.orders() 35 | 36 | # Get instruments 37 | kite.instruments() 38 | 39 | # Place an mutual fund order 40 | kite.place_mf_order( 41 | tradingsymbol="INF090I01239", 42 | transaction_type=kite.TRANSACTION_TYPE_BUY, 43 | amount=5000, 44 | tag="mytag" 45 | ) 46 | 47 | # Cancel a mutual fund order 48 | kite.cancel_mf_order(order_id="order_id") 49 | 50 | # Get mutual fund instruments 51 | kite.mf_instruments() 52 | -------------------------------------------------------------------------------- /examples/threaded_ticker.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) Zerodha Technology Pvt. Ltd. 6 | # 7 | # This example shows how to run KiteTicker in threaded mode. 8 | # KiteTicker runs in seprate thread and main thread is blocked to juggle between 9 | # different modes for current subscribed tokens. In real world web apps 10 | # the main thread will be your web server and you can access WebSocket object 11 | # in your main thread while running KiteTicker in separate thread. 12 | ############################################################################### 13 | 14 | import time 15 | import logging 16 | from kiteconnect import KiteTicker 17 | 18 | logging.basicConfig(level=logging.DEBUG) 19 | 20 | # Initialise. 21 | kws = KiteTicker("your_api_key", "your_access_token") 22 | 23 | # RELIANCE BSE 24 | tokens = [738561] 25 | 26 | 27 | # Callback for tick reception. 28 | def on_ticks(ws, ticks): 29 | if len(ticks) > 0: 30 | logging.info("Current mode: {}".format(ticks[0]["mode"])) 31 | 32 | 33 | # Callback for successful connection. 34 | def on_connect(ws, response): 35 | logging.info("Successfully connected. Response: {}".format(response)) 36 | ws.subscribe(tokens) 37 | ws.set_mode(ws.MODE_FULL, tokens) 38 | logging.info("Subscribe to tokens in Full mode: {}".format(tokens)) 39 | 40 | 41 | # Callback when current connection is closed. 42 | def on_close(ws, code, reason): 43 | logging.info("Connection closed: {code} - {reason}".format(code=code, reason=reason)) 44 | 45 | 46 | # Callback when connection closed with error. 47 | def on_error(ws, code, reason): 48 | logging.info("Connection error: {code} - {reason}".format(code=code, reason=reason)) 49 | 50 | 51 | # Callback when reconnect is on progress 52 | def on_reconnect(ws, attempts_count): 53 | logging.info("Reconnecting: {}".format(attempts_count)) 54 | 55 | 56 | # Callback when all reconnect failed (exhausted max retries) 57 | def on_noreconnect(ws): 58 | logging.info("Reconnect failed.") 59 | 60 | 61 | # Assign the callbacks. 62 | kws.on_ticks = on_ticks 63 | kws.on_close = on_close 64 | kws.on_error = on_error 65 | kws.on_connect = on_connect 66 | kws.on_reconnect = on_reconnect 67 | kws.on_noreconnect = on_noreconnect 68 | 69 | # Infinite loop on the main thread. 70 | # You have to use the pre-defined callbacks to manage subscriptions. 71 | kws.connect(threaded=True) 72 | 73 | # Block main thread 74 | logging.info("This is main thread. Will change webosocket mode every 5 seconds.") 75 | 76 | count = 0 77 | while True: 78 | count += 1 79 | if count % 2 == 0: 80 | if kws.is_connected(): 81 | logging.info("### Set mode to LTP for all tokens") 82 | kws.set_mode(kws.MODE_LTP, tokens) 83 | else: 84 | if kws.is_connected(): 85 | logging.info("### Set mode to quote for all tokens") 86 | kws.set_mode(kws.MODE_QUOTE, tokens) 87 | 88 | time.sleep(5) 89 | -------------------------------------------------------------------------------- /examples/ticker.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) Zerodha Technology Pvt. Ltd. 6 | # 7 | # This example shows how to subscribe and get ticks from Kite Connect ticker, 8 | # For more info read documentation - https://kite.trade/docs/connect/v1/#streaming-websocket 9 | ############################################################################### 10 | 11 | import logging 12 | from kiteconnect import KiteTicker 13 | 14 | logging.basicConfig(level=logging.DEBUG) 15 | 16 | # Initialise 17 | kws = KiteTicker("your_api_key", "your_access_token") 18 | 19 | def on_ticks(ws, ticks): # noqa 20 | # Callback to receive ticks. 21 | logging.info("Ticks: {}".format(ticks)) 22 | 23 | def on_connect(ws, response): # noqa 24 | # Callback on successful connect. 25 | # Subscribe to a list of instrument_tokens (RELIANCE and ACC here). 26 | ws.subscribe([738561, 5633]) 27 | 28 | # Set RELIANCE to tick in `full` mode. 29 | ws.set_mode(ws.MODE_FULL, [738561]) 30 | 31 | def on_order_update(ws, data): 32 | logging.debug("Order update : {}".format(data)) 33 | 34 | # Assign the callbacks. 35 | kws.on_ticks = on_ticks 36 | kws.on_connect = on_connect 37 | kws.on_order_update = on_order_update 38 | 39 | # Infinite loop on the main thread. Nothing after this will run. 40 | # You have to use the pre-defined callbacks to manage subscriptions. 41 | kws.connect() 42 | -------------------------------------------------------------------------------- /kiteconnect/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Kite Connect API client for Python -- [kite.trade](https://kite.trade). 4 | 5 | Zerodha Technology Pvt. Ltd. (c) 2021 6 | 7 | License 8 | ------- 9 | KiteConnect Python library is licensed under the MIT License 10 | 11 | The library 12 | ----------- 13 | Kite Connect is a set of REST-like APIs that expose 14 | many capabilities required to build a complete 15 | investment and trading platform. Execute orders in 16 | real time, manage user portfolio, stream live market 17 | data (WebSockets), and more, with the simple HTTP API collection 18 | 19 | This module provides an easy to use abstraction over the HTTP APIs. 20 | The HTTP calls have been converted to methods and their JSON responses 21 | are returned as native Python structures, for example, dicts, lists, bools etc. 22 | See the **[Kite Connect API documentation](https://kite.trade/docs/connect/v3/)** 23 | for the complete list of APIs, supported parameters and values, and response formats. 24 | 25 | Getting started 26 | --------------- 27 | #!python 28 | import logging 29 | from kiteconnect import KiteConnect 30 | 31 | logging.basicConfig(level=logging.DEBUG) 32 | 33 | kite = KiteConnect(api_key="your_api_key") 34 | 35 | # Redirect the user to the login url obtained 36 | # from kite.login_url(), and receive the request_token 37 | # from the registered redirect url after the login flow. 38 | # Once you have the request_token, obtain the access_token 39 | # as follows. 40 | 41 | data = kite.generate_session("request_token_here", api_secret="your_secret") 42 | kite.set_access_token(data["access_token"]) 43 | 44 | # Place an order 45 | try: 46 | order_id = kite.place_order(variety=kite.VARIETY_REGULAR, 47 | tradingsymbol="INFY", 48 | exchange=kite.EXCHANGE_NSE, 49 | transaction_type=kite.TRANSACTION_TYPE_BUY, 50 | quantity=1, 51 | order_type=kite.ORDER_TYPE_MARKET, 52 | product=kite.PRODUCT_CNC, 53 | validity=kite.VALIDITY_DAY) 54 | 55 | logging.info("Order placed. ID is: {}".format(order_id)) 56 | except Exception as e: 57 | logging.info("Order placement failed: {}".format(e.message)) 58 | 59 | # Fetch all orders 60 | kite.orders() 61 | 62 | # Get instruments 63 | kite.instruments() 64 | 65 | # Place an mutual fund order 66 | kite.place_mf_order( 67 | tradingsymbol="INF090I01239", 68 | transaction_type=kite.TRANSACTION_TYPE_BUY, 69 | amount=5000, 70 | tag="mytag" 71 | ) 72 | 73 | # Cancel a mutual fund order 74 | kite.cancel_mf_order(order_id="order_id") 75 | 76 | # Get mutual fund instruments 77 | kite.mf_instruments() 78 | 79 | A typical web application 80 | ------------------------- 81 | In a typical web application where a new instance of 82 | views, controllers etc. are created per incoming HTTP 83 | request, you will need to initialise a new instance of 84 | Kite client per request as well. This is because each 85 | individual instance represents a single user that's 86 | authenticated, unlike an **admin** API where you may 87 | use one instance to manage many users. 88 | 89 | Hence, in your web application, typically: 90 | 91 | - You will initialise an instance of the Kite client 92 | - Redirect the user to the `login_url()` 93 | - At the redirect url endpoint, obtain the 94 | `request_token` from the query parameters 95 | - Initialise a new instance of Kite client, 96 | use `generate_session()` to obtain the `access_token` 97 | along with authenticated user data 98 | - Store this response in a session and use the 99 | stored `access_token` and initialise instances 100 | of Kite client for subsequent API calls. 101 | 102 | Exceptions 103 | ---------- 104 | Kite Connect client saves you the hassle of detecting API errors 105 | by looking at HTTP codes or JSON error responses. Instead, 106 | it raises aptly named **[exceptions](exceptions.m.html)** that you can catch. 107 | """ 108 | 109 | from __future__ import unicode_literals, absolute_import 110 | 111 | from kiteconnect import exceptions 112 | from kiteconnect.connect import KiteConnect 113 | from kiteconnect.ticker import KiteTicker 114 | 115 | __all__ = ["KiteConnect", "KiteTicker", "exceptions"] 116 | -------------------------------------------------------------------------------- /kiteconnect/__version__.py: -------------------------------------------------------------------------------- 1 | __title__ = "kiteconnect" 2 | __description__ = "The official Python client for the Kite Connect trading API" 3 | __url__ = "https://kite.trade" 4 | __download_url__ = "https://github.com/zerodhatech/pykiteconnect" 5 | __version__ = "5.0.1" 6 | __author__ = "Zerodha Technology Pvt. Ltd. (India)" 7 | __author_email__ = "talk@zerodha.tech" 8 | __license__ = "MIT" 9 | -------------------------------------------------------------------------------- /kiteconnect/connect.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | connect.py 4 | 5 | API wrapper for Kite Connect REST APIs. 6 | 7 | :copyright: (c) 2021 by Zerodha Technology. 8 | :license: see LICENSE for details. 9 | """ 10 | from six import StringIO, PY2 11 | from six.moves.urllib.parse import urljoin 12 | import csv 13 | import json 14 | import dateutil.parser 15 | import hashlib 16 | import logging 17 | import datetime 18 | import requests 19 | import warnings 20 | 21 | from .__version__ import __version__, __title__ 22 | import kiteconnect.exceptions as ex 23 | 24 | log = logging.getLogger(__name__) 25 | 26 | 27 | class KiteConnect(object): 28 | """ 29 | The Kite Connect API wrapper class. 30 | 31 | In production, you may initialise a single instance of this class per `api_key`. 32 | """ 33 | 34 | # Default root API endpoint. It's possible to 35 | # override this by passing the `root` parameter during initialisation. 36 | _default_root_uri = "https://api.kite.trade" 37 | _default_login_uri = "https://kite.zerodha.com/connect/login" 38 | _default_timeout = 7 # In seconds 39 | 40 | # Kite connect header version 41 | kite_header_version = "3" 42 | 43 | # Constants 44 | # Products 45 | PRODUCT_MIS = "MIS" 46 | PRODUCT_CNC = "CNC" 47 | PRODUCT_NRML = "NRML" 48 | PRODUCT_CO = "CO" 49 | 50 | # Order types 51 | ORDER_TYPE_MARKET = "MARKET" 52 | ORDER_TYPE_LIMIT = "LIMIT" 53 | ORDER_TYPE_SLM = "SL-M" 54 | ORDER_TYPE_SL = "SL" 55 | 56 | # Varities 57 | VARIETY_REGULAR = "regular" 58 | VARIETY_CO = "co" 59 | VARIETY_AMO = "amo" 60 | VARIETY_ICEBERG = "iceberg" 61 | VARIETY_AUCTION = "auction" 62 | 63 | # Transaction type 64 | TRANSACTION_TYPE_BUY = "BUY" 65 | TRANSACTION_TYPE_SELL = "SELL" 66 | 67 | # Validity 68 | VALIDITY_DAY = "DAY" 69 | VALIDITY_IOC = "IOC" 70 | VALIDITY_TTL = "TTL" 71 | 72 | # Position Type 73 | POSITION_TYPE_DAY = "day" 74 | POSITION_TYPE_OVERNIGHT = "overnight" 75 | 76 | # Exchanges 77 | EXCHANGE_NSE = "NSE" 78 | EXCHANGE_BSE = "BSE" 79 | EXCHANGE_NFO = "NFO" 80 | EXCHANGE_CDS = "CDS" 81 | EXCHANGE_BFO = "BFO" 82 | EXCHANGE_MCX = "MCX" 83 | EXCHANGE_BCD = "BCD" 84 | 85 | # Margins segments 86 | MARGIN_EQUITY = "equity" 87 | MARGIN_COMMODITY = "commodity" 88 | 89 | # Status constants 90 | STATUS_COMPLETE = "COMPLETE" 91 | STATUS_REJECTED = "REJECTED" 92 | STATUS_CANCELLED = "CANCELLED" 93 | 94 | # GTT order type 95 | GTT_TYPE_OCO = "two-leg" 96 | GTT_TYPE_SINGLE = "single" 97 | 98 | # GTT order status 99 | GTT_STATUS_ACTIVE = "active" 100 | GTT_STATUS_TRIGGERED = "triggered" 101 | GTT_STATUS_DISABLED = "disabled" 102 | GTT_STATUS_EXPIRED = "expired" 103 | GTT_STATUS_CANCELLED = "cancelled" 104 | GTT_STATUS_REJECTED = "rejected" 105 | GTT_STATUS_DELETED = "deleted" 106 | 107 | # URIs to various calls 108 | _routes = { 109 | "api.token": "/session/token", 110 | "api.token.invalidate": "/session/token", 111 | "api.token.renew": "/session/refresh_token", 112 | "user.profile": "/user/profile", 113 | "user.margins": "/user/margins", 114 | "user.margins.segment": "/user/margins/{segment}", 115 | 116 | "orders": "/orders", 117 | "trades": "/trades", 118 | 119 | "order.info": "/orders/{order_id}", 120 | "order.place": "/orders/{variety}", 121 | "order.modify": "/orders/{variety}/{order_id}", 122 | "order.cancel": "/orders/{variety}/{order_id}", 123 | "order.trades": "/orders/{order_id}/trades", 124 | 125 | "portfolio.positions": "/portfolio/positions", 126 | "portfolio.holdings": "/portfolio/holdings", 127 | "portfolio.holdings.auction": "/portfolio/holdings/auctions", 128 | "portfolio.positions.convert": "/portfolio/positions", 129 | 130 | # MF api endpoints 131 | "mf.orders": "/mf/orders", 132 | "mf.order.info": "/mf/orders/{order_id}", 133 | "mf.order.place": "/mf/orders", 134 | "mf.order.cancel": "/mf/orders/{order_id}", 135 | 136 | "mf.sips": "/mf/sips", 137 | "mf.sip.info": "/mf/sips/{sip_id}", 138 | "mf.sip.place": "/mf/sips", 139 | "mf.sip.modify": "/mf/sips/{sip_id}", 140 | "mf.sip.cancel": "/mf/sips/{sip_id}", 141 | 142 | "mf.holdings": "/mf/holdings", 143 | "mf.instruments": "/mf/instruments", 144 | 145 | "market.instruments.all": "/instruments", 146 | "market.instruments": "/instruments/{exchange}", 147 | "market.margins": "/margins/{segment}", 148 | "market.historical": "/instruments/historical/{instrument_token}/{interval}", 149 | "market.trigger_range": "/instruments/trigger_range/{transaction_type}", 150 | 151 | "market.quote": "/quote", 152 | "market.quote.ohlc": "/quote/ohlc", 153 | "market.quote.ltp": "/quote/ltp", 154 | 155 | # GTT endpoints 156 | "gtt": "/gtt/triggers", 157 | "gtt.place": "/gtt/triggers", 158 | "gtt.info": "/gtt/triggers/{trigger_id}", 159 | "gtt.modify": "/gtt/triggers/{trigger_id}", 160 | "gtt.delete": "/gtt/triggers/{trigger_id}", 161 | 162 | # Margin computation endpoints 163 | "order.margins": "/margins/orders", 164 | "order.margins.basket": "/margins/basket", 165 | "order.contract_note": "/charges/orders", 166 | } 167 | 168 | def __init__(self, 169 | api_key, 170 | access_token=None, 171 | root=None, 172 | debug=False, 173 | timeout=None, 174 | proxies=None, 175 | pool=None, 176 | disable_ssl=False): 177 | """ 178 | Initialise a new Kite Connect client instance. 179 | 180 | - `api_key` is the key issued to you 181 | - `access_token` is the token obtained after the login flow in 182 | exchange for the `request_token` . Pre-login, this will default to None, 183 | but once you have obtained it, you should 184 | persist it in a database or session to pass 185 | to the Kite Connect class initialisation for subsequent requests. 186 | - `root` is the API end point root. Unless you explicitly 187 | want to send API requests to a non-default endpoint, this 188 | can be ignored. 189 | - `debug`, if set to True, will serialise and print requests 190 | and responses to stdout. 191 | - `timeout` is the time (seconds) for which the API client will wait for 192 | a request to complete before it fails. Defaults to 7 seconds 193 | - `proxies` to set requests proxy. 194 | Check [python requests documentation](http://docs.python-requests.org/en/master/user/advanced/#proxies) for usage and examples. 195 | - `pool` is manages request pools. It takes a dict of params accepted by HTTPAdapter as described here in [python requests documentation](http://docs.python-requests.org/en/master/api/#requests.adapters.HTTPAdapter) 196 | - `disable_ssl` disables the SSL verification while making a request. 197 | If set requests won't throw SSLError if its set to custom `root` url without SSL. 198 | """ 199 | self.debug = debug 200 | self.api_key = api_key 201 | self.session_expiry_hook = None 202 | self.disable_ssl = disable_ssl 203 | self.access_token = access_token 204 | self.proxies = proxies if proxies else {} 205 | 206 | self.root = root or self._default_root_uri 207 | self.timeout = timeout or self._default_timeout 208 | 209 | # Create requests session by default 210 | # Same session to be used by pool connections 211 | self.reqsession = requests.Session() 212 | if pool: 213 | reqadapter = requests.adapters.HTTPAdapter(**pool) 214 | self.reqsession.mount("https://", reqadapter) 215 | 216 | # disable requests SSL warning 217 | requests.packages.urllib3.disable_warnings() 218 | 219 | def set_session_expiry_hook(self, method): 220 | """ 221 | Set a callback hook for session (`TokenError` -- timeout, expiry etc.) errors. 222 | 223 | An `access_token` (login session) can become invalid for a number of 224 | reasons, but it doesn't make sense for the client to 225 | try and catch it during every API call. 226 | 227 | A callback method that handles session errors 228 | can be set here and when the client encounters 229 | a token error at any point, it'll be called. 230 | 231 | This callback, for instance, can log the user out of the UI, 232 | clear session cookies, or initiate a fresh login. 233 | """ 234 | if not callable(method): 235 | raise TypeError("Invalid input type. Only functions are accepted.") 236 | 237 | self.session_expiry_hook = method 238 | 239 | def set_access_token(self, access_token): 240 | """Set the `access_token` received after a successful authentication.""" 241 | self.access_token = access_token 242 | 243 | def login_url(self): 244 | """Get the remote login url to which a user should be redirected to initiate the login flow.""" 245 | return "%s?api_key=%s&v=%s" % (self._default_login_uri, self.api_key, self.kite_header_version) 246 | 247 | def generate_session(self, request_token, api_secret): 248 | """ 249 | Generate user session details like `access_token` etc by exchanging `request_token`. 250 | Access token is automatically set if the session is retrieved successfully. 251 | 252 | Do the token exchange with the `request_token` obtained after the login flow, 253 | and retrieve the `access_token` required for all subsequent requests. The 254 | response contains not just the `access_token`, but metadata for 255 | the user who has authenticated. 256 | 257 | - `request_token` is the token obtained from the GET paramers after a successful login redirect. 258 | - `api_secret` is the API api_secret issued with the API key. 259 | """ 260 | h = hashlib.sha256(self.api_key.encode("utf-8") + request_token.encode("utf-8") + api_secret.encode("utf-8")) 261 | checksum = h.hexdigest() 262 | 263 | resp = self._post("api.token", params={ 264 | "api_key": self.api_key, 265 | "request_token": request_token, 266 | "checksum": checksum 267 | }) 268 | 269 | if "access_token" in resp: 270 | self.set_access_token(resp["access_token"]) 271 | 272 | if resp["login_time"] and len(resp["login_time"]) == 19: 273 | resp["login_time"] = dateutil.parser.parse(resp["login_time"]) 274 | 275 | return resp 276 | 277 | def invalidate_access_token(self, access_token=None): 278 | """ 279 | Kill the session by invalidating the access token. 280 | 281 | - `access_token` to invalidate. Default is the active `access_token`. 282 | """ 283 | access_token = access_token or self.access_token 284 | return self._delete("api.token.invalidate", params={ 285 | "api_key": self.api_key, 286 | "access_token": access_token 287 | }) 288 | 289 | def renew_access_token(self, refresh_token, api_secret): 290 | """ 291 | Renew expired `refresh_token` using valid `refresh_token`. 292 | 293 | - `refresh_token` is the token obtained from previous successful login flow. 294 | - `api_secret` is the API api_secret issued with the API key. 295 | """ 296 | h = hashlib.sha256(self.api_key.encode("utf-8") + refresh_token.encode("utf-8") + api_secret.encode("utf-8")) 297 | checksum = h.hexdigest() 298 | 299 | resp = self._post("api.token.renew", params={ 300 | "api_key": self.api_key, 301 | "refresh_token": refresh_token, 302 | "checksum": checksum 303 | }) 304 | 305 | if "access_token" in resp: 306 | self.set_access_token(resp["access_token"]) 307 | 308 | return resp 309 | 310 | def invalidate_refresh_token(self, refresh_token): 311 | """ 312 | Invalidate refresh token. 313 | 314 | - `refresh_token` is the token which is used to renew access token. 315 | """ 316 | return self._delete("api.token.invalidate", params={ 317 | "api_key": self.api_key, 318 | "refresh_token": refresh_token 319 | }) 320 | 321 | def margins(self, segment=None): 322 | """Get account balance and cash margin details for a particular segment. 323 | 324 | - `segment` is the trading segment (eg: equity or commodity) 325 | """ 326 | if segment: 327 | return self._get("user.margins.segment", url_args={"segment": segment}) 328 | else: 329 | return self._get("user.margins") 330 | 331 | def profile(self): 332 | """Get user profile details.""" 333 | return self._get("user.profile") 334 | 335 | # orders 336 | def place_order(self, 337 | variety, 338 | exchange, 339 | tradingsymbol, 340 | transaction_type, 341 | quantity, 342 | product, 343 | order_type, 344 | price=None, 345 | validity=None, 346 | validity_ttl=None, 347 | disclosed_quantity=None, 348 | trigger_price=None, 349 | iceberg_legs=None, 350 | iceberg_quantity=None, 351 | auction_number=None, 352 | tag=None): 353 | """Place an order.""" 354 | params = locals() 355 | del (params["self"]) 356 | 357 | for k in list(params.keys()): 358 | if params[k] is None: 359 | del (params[k]) 360 | 361 | return self._post("order.place", 362 | url_args={"variety": variety}, 363 | params=params)["order_id"] 364 | 365 | def modify_order(self, 366 | variety, 367 | order_id, 368 | parent_order_id=None, 369 | quantity=None, 370 | price=None, 371 | order_type=None, 372 | trigger_price=None, 373 | validity=None, 374 | disclosed_quantity=None): 375 | """Modify an open order.""" 376 | params = locals() 377 | del (params["self"]) 378 | 379 | for k in list(params.keys()): 380 | if params[k] is None: 381 | del (params[k]) 382 | 383 | return self._put("order.modify", 384 | url_args={"variety": variety, "order_id": order_id}, 385 | params=params)["order_id"] 386 | 387 | def cancel_order(self, variety, order_id, parent_order_id=None): 388 | """Cancel an order.""" 389 | return self._delete("order.cancel", 390 | url_args={"variety": variety, "order_id": order_id}, 391 | params={"parent_order_id": parent_order_id})["order_id"] 392 | 393 | def exit_order(self, variety, order_id, parent_order_id=None): 394 | """Exit a CO order.""" 395 | return self.cancel_order(variety, order_id, parent_order_id=parent_order_id) 396 | 397 | def _format_response(self, data): 398 | """Parse and format responses.""" 399 | 400 | if type(data) == list: 401 | _list = data 402 | elif type(data) == dict: 403 | _list = [data] 404 | 405 | for item in _list: 406 | # Convert date time string to datetime object 407 | for field in ["order_timestamp", "exchange_timestamp", "created", "last_instalment", "fill_timestamp", "timestamp", "last_trade_time"]: 408 | if item.get(field) and len(item[field]) == 19: 409 | item[field] = dateutil.parser.parse(item[field]) 410 | 411 | return _list[0] if type(data) == dict else _list 412 | 413 | # orderbook and tradebook 414 | def orders(self): 415 | """Get list of orders.""" 416 | return self._format_response(self._get("orders")) 417 | 418 | def order_history(self, order_id): 419 | """ 420 | Get history of individual order. 421 | 422 | - `order_id` is the ID of the order to retrieve order history. 423 | """ 424 | return self._format_response(self._get("order.info", url_args={"order_id": order_id})) 425 | 426 | def trades(self): 427 | """ 428 | Retrieve the list of trades executed (all or ones under a particular order). 429 | 430 | An order can be executed in tranches based on market conditions. 431 | These trades are individually recorded under an order. 432 | """ 433 | return self._format_response(self._get("trades")) 434 | 435 | def order_trades(self, order_id): 436 | """ 437 | Retrieve the list of trades executed for a particular order. 438 | 439 | - `order_id` is the ID of the order to retrieve trade history. 440 | """ 441 | return self._format_response(self._get("order.trades", url_args={"order_id": order_id})) 442 | 443 | def positions(self): 444 | """Retrieve the list of positions.""" 445 | return self._get("portfolio.positions") 446 | 447 | def holdings(self): 448 | """Retrieve the list of equity holdings.""" 449 | return self._get("portfolio.holdings") 450 | 451 | def get_auction_instruments(self): 452 | """ Retrieves list of available instruments for a auction session """ 453 | return self._get("portfolio.holdings.auction") 454 | 455 | def convert_position(self, 456 | exchange, 457 | tradingsymbol, 458 | transaction_type, 459 | position_type, 460 | quantity, 461 | old_product, 462 | new_product): 463 | """Modify an open position's product type.""" 464 | return self._put("portfolio.positions.convert", params={ 465 | "exchange": exchange, 466 | "tradingsymbol": tradingsymbol, 467 | "transaction_type": transaction_type, 468 | "position_type": position_type, 469 | "quantity": quantity, 470 | "old_product": old_product, 471 | "new_product": new_product 472 | }) 473 | 474 | def mf_orders(self, order_id=None): 475 | """Get all mutual fund orders or individual order info.""" 476 | if order_id: 477 | return self._format_response(self._get("mf.order.info", url_args={"order_id": order_id})) 478 | else: 479 | return self._format_response(self._get("mf.orders")) 480 | 481 | def place_mf_order(self, 482 | tradingsymbol, 483 | transaction_type, 484 | quantity=None, 485 | amount=None, 486 | tag=None): 487 | """Place a mutual fund order.""" 488 | return self._post("mf.order.place", params={ 489 | "tradingsymbol": tradingsymbol, 490 | "transaction_type": transaction_type, 491 | "quantity": quantity, 492 | "amount": amount, 493 | "tag": tag 494 | }) 495 | 496 | def cancel_mf_order(self, order_id): 497 | """Cancel a mutual fund order.""" 498 | return self._delete("mf.order.cancel", url_args={"order_id": order_id}) 499 | 500 | def mf_sips(self, sip_id=None): 501 | """Get list of all mutual fund SIP's or individual SIP info.""" 502 | if sip_id: 503 | return self._format_response(self._get("mf.sip.info", url_args={"sip_id": sip_id})) 504 | else: 505 | return self._format_response(self._get("mf.sips")) 506 | 507 | def place_mf_sip(self, 508 | tradingsymbol, 509 | amount, 510 | instalments, 511 | frequency, 512 | initial_amount=None, 513 | instalment_day=None, 514 | tag=None): 515 | """Place a mutual fund SIP.""" 516 | return self._post("mf.sip.place", params={ 517 | "tradingsymbol": tradingsymbol, 518 | "amount": amount, 519 | "initial_amount": initial_amount, 520 | "instalments": instalments, 521 | "frequency": frequency, 522 | "instalment_day": instalment_day, 523 | "tag": tag 524 | }) 525 | 526 | def modify_mf_sip(self, 527 | sip_id, 528 | amount=None, 529 | status=None, 530 | instalments=None, 531 | frequency=None, 532 | instalment_day=None): 533 | """Modify a mutual fund SIP.""" 534 | return self._put("mf.sip.modify", 535 | url_args={"sip_id": sip_id}, 536 | params={ 537 | "amount": amount, 538 | "status": status, 539 | "instalments": instalments, 540 | "frequency": frequency, 541 | "instalment_day": instalment_day 542 | }) 543 | 544 | def cancel_mf_sip(self, sip_id): 545 | """Cancel a mutual fund SIP.""" 546 | return self._delete("mf.sip.cancel", url_args={"sip_id": sip_id}) 547 | 548 | def mf_holdings(self): 549 | """Get list of mutual fund holdings.""" 550 | return self._get("mf.holdings") 551 | 552 | def mf_instruments(self): 553 | """Get list of mutual fund instruments.""" 554 | return self._parse_mf_instruments(self._get("mf.instruments")) 555 | 556 | def instruments(self, exchange=None): 557 | """ 558 | Retrieve the list of market instruments available to trade. 559 | 560 | Note that the results could be large, several hundred KBs in size, 561 | with tens of thousands of entries in the list. 562 | 563 | - `exchange` is specific exchange to fetch (Optional) 564 | """ 565 | if exchange: 566 | return self._parse_instruments(self._get("market.instruments", url_args={"exchange": exchange})) 567 | else: 568 | return self._parse_instruments(self._get("market.instruments.all")) 569 | 570 | def quote(self, *instruments): 571 | """ 572 | Retrieve quote for list of instruments. 573 | 574 | - `instruments` is a list of instruments, Instrument are in the format of `exchange:tradingsymbol`. For example NSE:INFY 575 | """ 576 | ins = list(instruments) 577 | 578 | # If first element is a list then accept it as instruments list for legacy reason 579 | if len(instruments) > 0 and type(instruments[0]) == list: 580 | ins = instruments[0] 581 | 582 | data = self._get("market.quote", params={"i": ins}) 583 | return {key: self._format_response(data[key]) for key in data} 584 | 585 | def ohlc(self, *instruments): 586 | """ 587 | Retrieve OHLC and market depth for list of instruments. 588 | 589 | - `instruments` is a list of instruments, Instrument are in the format of `exchange:tradingsymbol`. For example NSE:INFY 590 | """ 591 | ins = list(instruments) 592 | 593 | # If first element is a list then accept it as instruments list for legacy reason 594 | if len(instruments) > 0 and type(instruments[0]) == list: 595 | ins = instruments[0] 596 | 597 | return self._get("market.quote.ohlc", params={"i": ins}) 598 | 599 | def ltp(self, *instruments): 600 | """ 601 | Retrieve last price for list of instruments. 602 | 603 | - `instruments` is a list of instruments, Instrument are in the format of `exchange:tradingsymbol`. For example NSE:INFY 604 | """ 605 | ins = list(instruments) 606 | 607 | # If first element is a list then accept it as instruments list for legacy reason 608 | if len(instruments) > 0 and type(instruments[0]) == list: 609 | ins = instruments[0] 610 | 611 | return self._get("market.quote.ltp", params={"i": ins}) 612 | 613 | def historical_data(self, instrument_token, from_date, to_date, interval, continuous=False, oi=False): 614 | """ 615 | Retrieve historical data (candles) for an instrument. 616 | 617 | Although the actual response JSON from the API does not have field 618 | names such has 'open', 'high' etc., this function call structures 619 | the data into an array of objects with field names. For example: 620 | 621 | - `instrument_token` is the instrument identifier (retrieved from the instruments()) call. 622 | - `from_date` is the From date (datetime object or string in format of yyyy-mm-dd HH:MM:SS. 623 | - `to_date` is the To date (datetime object or string in format of yyyy-mm-dd HH:MM:SS). 624 | - `interval` is the candle interval (minute, day, 5 minute etc.). 625 | - `continuous` is a boolean flag to get continuous data for futures and options instruments. 626 | - `oi` is a boolean flag to get open interest. 627 | """ 628 | date_string_format = "%Y-%m-%d %H:%M:%S" 629 | from_date_string = from_date.strftime(date_string_format) if type(from_date) == datetime.datetime else from_date 630 | to_date_string = to_date.strftime(date_string_format) if type(to_date) == datetime.datetime else to_date 631 | 632 | data = self._get("market.historical", 633 | url_args={"instrument_token": instrument_token, "interval": interval}, 634 | params={ 635 | "from": from_date_string, 636 | "to": to_date_string, 637 | "interval": interval, 638 | "continuous": 1 if continuous else 0, 639 | "oi": 1 if oi else 0 640 | }) 641 | 642 | return self._format_historical(data) 643 | 644 | def _format_historical(self, data): 645 | records = [] 646 | for d in data["candles"]: 647 | record = { 648 | "date": dateutil.parser.parse(d[0]), 649 | "open": d[1], 650 | "high": d[2], 651 | "low": d[3], 652 | "close": d[4], 653 | "volume": d[5], 654 | } 655 | if len(d) == 7: 656 | record["oi"] = d[6] 657 | records.append(record) 658 | 659 | return records 660 | 661 | def trigger_range(self, transaction_type, *instruments): 662 | """Retrieve the buy/sell trigger range for Cover Orders.""" 663 | ins = list(instruments) 664 | 665 | # If first element is a list then accept it as instruments list for legacy reason 666 | if len(instruments) > 0 and type(instruments[0]) == list: 667 | ins = instruments[0] 668 | 669 | return self._get("market.trigger_range", 670 | url_args={"transaction_type": transaction_type.lower()}, 671 | params={"i": ins}) 672 | 673 | def get_gtts(self): 674 | """Fetch list of gtt existing in an account""" 675 | return self._get("gtt") 676 | 677 | def get_gtt(self, trigger_id): 678 | """Fetch details of a GTT""" 679 | return self._get("gtt.info", url_args={"trigger_id": trigger_id}) 680 | 681 | def _get_gtt_payload(self, trigger_type, tradingsymbol, exchange, trigger_values, last_price, orders): 682 | """Get GTT payload""" 683 | if type(trigger_values) != list: 684 | raise ex.InputException("invalid type for `trigger_values`") 685 | if trigger_type == self.GTT_TYPE_SINGLE and len(trigger_values) != 1: 686 | raise ex.InputException("invalid `trigger_values` for single leg order type") 687 | elif trigger_type == self.GTT_TYPE_OCO and len(trigger_values) != 2: 688 | raise ex.InputException("invalid `trigger_values` for OCO order type") 689 | 690 | condition = { 691 | "exchange": exchange, 692 | "tradingsymbol": tradingsymbol, 693 | "trigger_values": trigger_values, 694 | "last_price": last_price, 695 | } 696 | 697 | gtt_orders = [] 698 | for o in orders: 699 | # Assert required keys inside gtt order. 700 | for req in ["transaction_type", "quantity", "order_type", "product", "price"]: 701 | if req not in o: 702 | raise ex.InputException("`{req}` missing inside orders".format(req=req)) 703 | gtt_orders.append({ 704 | "exchange": exchange, 705 | "tradingsymbol": tradingsymbol, 706 | "transaction_type": o["transaction_type"], 707 | "quantity": int(o["quantity"]), 708 | "order_type": o["order_type"], 709 | "product": o["product"], 710 | "price": float(o["price"]), 711 | }) 712 | 713 | return condition, gtt_orders 714 | 715 | def place_gtt( 716 | self, trigger_type, tradingsymbol, exchange, trigger_values, last_price, orders 717 | ): 718 | """ 719 | Place GTT order 720 | 721 | - `trigger_type` The type of GTT order(single/two-leg). 722 | - `tradingsymbol` Trading symbol of the instrument. 723 | - `exchange` Name of the exchange. 724 | - `trigger_values` Trigger values (json array). 725 | - `last_price` Last price of the instrument at the time of order placement. 726 | - `orders` JSON order array containing following fields 727 | - `transaction_type` BUY or SELL 728 | - `quantity` Quantity to transact 729 | - `price` The min or max price to execute the order at (for LIMIT orders) 730 | """ 731 | # Validations. 732 | assert trigger_type in [self.GTT_TYPE_OCO, self.GTT_TYPE_SINGLE] 733 | condition, gtt_orders = self._get_gtt_payload(trigger_type, tradingsymbol, exchange, trigger_values, last_price, orders) 734 | 735 | return self._post("gtt.place", params={ 736 | "condition": json.dumps(condition), 737 | "orders": json.dumps(gtt_orders), 738 | "type": trigger_type}) 739 | 740 | def modify_gtt( 741 | self, trigger_id, trigger_type, tradingsymbol, exchange, trigger_values, last_price, orders 742 | ): 743 | """ 744 | Modify GTT order 745 | 746 | - `trigger_type` The type of GTT order(single/two-leg). 747 | - `tradingsymbol` Trading symbol of the instrument. 748 | - `exchange` Name of the exchange. 749 | - `trigger_values` Trigger values (json array). 750 | - `last_price` Last price of the instrument at the time of order placement. 751 | - `orders` JSON order array containing following fields 752 | - `transaction_type` BUY or SELL 753 | - `quantity` Quantity to transact 754 | - `price` The min or max price to execute the order at (for LIMIT orders) 755 | """ 756 | condition, gtt_orders = self._get_gtt_payload(trigger_type, tradingsymbol, exchange, trigger_values, last_price, orders) 757 | 758 | return self._put("gtt.modify", 759 | url_args={"trigger_id": trigger_id}, 760 | params={ 761 | "condition": json.dumps(condition), 762 | "orders": json.dumps(gtt_orders), 763 | "type": trigger_type}) 764 | 765 | def delete_gtt(self, trigger_id): 766 | """Delete a GTT order.""" 767 | return self._delete("gtt.delete", url_args={"trigger_id": trigger_id}) 768 | 769 | def order_margins(self, params): 770 | """ 771 | Calculate margins for requested order list considering the existing positions and open orders 772 | 773 | - `params` is list of orders to retrive margins detail 774 | """ 775 | return self._post("order.margins", params=params, is_json=True) 776 | 777 | def basket_order_margins(self, params, consider_positions=True, mode=None): 778 | """ 779 | Calculate total margins required for basket of orders including margin benefits 780 | 781 | - `params` is list of orders to fetch basket margin 782 | - `consider_positions` is a boolean to consider users positions 783 | - `mode` is margin response mode type. compact - Compact mode will only give the total margins 784 | """ 785 | return self._post("order.margins.basket", 786 | params=params, 787 | is_json=True, 788 | query_params={'consider_positions': consider_positions, 'mode': mode}) 789 | 790 | def get_virtual_contract_note(self, params): 791 | """ 792 | Calculates detailed charges order-wise for the order book 793 | - `params` is list of orders to fetch charges detail 794 | """ 795 | return self._post("order.contract_note", 796 | params=params, 797 | is_json=True) 798 | 799 | def _warn(self, message): 800 | """ Add deprecation warning message """ 801 | warnings.simplefilter('always', DeprecationWarning) 802 | warnings.warn(message, DeprecationWarning) 803 | 804 | def _parse_instruments(self, data): 805 | # decode to string for Python 3 806 | d = data 807 | # Decode unicode data 808 | if not PY2 and type(d) == bytes: 809 | d = data.decode("utf-8").strip() 810 | 811 | records = [] 812 | reader = csv.DictReader(StringIO(d)) 813 | 814 | for row in reader: 815 | row["instrument_token"] = int(row["instrument_token"]) 816 | row["last_price"] = float(row["last_price"]) 817 | row["strike"] = float(row["strike"]) 818 | row["tick_size"] = float(row["tick_size"]) 819 | row["lot_size"] = int(row["lot_size"]) 820 | 821 | # Parse date 822 | if len(row["expiry"]) == 10: 823 | row["expiry"] = dateutil.parser.parse(row["expiry"]).date() 824 | 825 | records.append(row) 826 | 827 | return records 828 | 829 | def _parse_mf_instruments(self, data): 830 | # decode to string for Python 3 831 | d = data 832 | if not PY2 and type(d) == bytes: 833 | d = data.decode("utf-8").strip() 834 | 835 | records = [] 836 | reader = csv.DictReader(StringIO(d)) 837 | 838 | for row in reader: 839 | row["minimum_purchase_amount"] = float(row["minimum_purchase_amount"]) 840 | row["purchase_amount_multiplier"] = float(row["purchase_amount_multiplier"]) 841 | row["minimum_additional_purchase_amount"] = float(row["minimum_additional_purchase_amount"]) 842 | row["minimum_redemption_quantity"] = float(row["minimum_redemption_quantity"]) 843 | row["redemption_quantity_multiplier"] = float(row["redemption_quantity_multiplier"]) 844 | row["purchase_allowed"] = bool(int(row["purchase_allowed"])) 845 | row["redemption_allowed"] = bool(int(row["redemption_allowed"])) 846 | row["last_price"] = float(row["last_price"]) 847 | 848 | # Parse date 849 | if len(row["last_price_date"]) == 10: 850 | row["last_price_date"] = dateutil.parser.parse(row["last_price_date"]).date() 851 | 852 | records.append(row) 853 | 854 | return records 855 | 856 | def _user_agent(self): 857 | return (__title__ + "-python/").capitalize() + __version__ 858 | 859 | def _get(self, route, url_args=None, params=None, is_json=False): 860 | """Alias for sending a GET request.""" 861 | return self._request(route, "GET", url_args=url_args, params=params, is_json=is_json) 862 | 863 | def _post(self, route, url_args=None, params=None, is_json=False, query_params=None): 864 | """Alias for sending a POST request.""" 865 | return self._request(route, "POST", url_args=url_args, params=params, is_json=is_json, query_params=query_params) 866 | 867 | def _put(self, route, url_args=None, params=None, is_json=False, query_params=None): 868 | """Alias for sending a PUT request.""" 869 | return self._request(route, "PUT", url_args=url_args, params=params, is_json=is_json, query_params=query_params) 870 | 871 | def _delete(self, route, url_args=None, params=None, is_json=False): 872 | """Alias for sending a DELETE request.""" 873 | return self._request(route, "DELETE", url_args=url_args, params=params, is_json=is_json) 874 | 875 | def _request(self, route, method, url_args=None, params=None, is_json=False, query_params=None): 876 | """Make an HTTP request.""" 877 | # Form a restful URL 878 | if url_args: 879 | uri = self._routes[route].format(**url_args) 880 | else: 881 | uri = self._routes[route] 882 | 883 | url = urljoin(self.root, uri) 884 | 885 | # Custom headers 886 | headers = { 887 | "X-Kite-Version": self.kite_header_version, 888 | "User-Agent": self._user_agent() 889 | } 890 | 891 | if self.api_key and self.access_token: 892 | # set authorization header 893 | auth_header = self.api_key + ":" + self.access_token 894 | headers["Authorization"] = "token {}".format(auth_header) 895 | 896 | if self.debug: 897 | log.debug("Request: {method} {url} {params} {headers}".format(method=method, url=url, params=params, headers=headers)) 898 | 899 | # prepare url query params 900 | if method in ["GET", "DELETE"]: 901 | query_params = params 902 | 903 | try: 904 | r = self.reqsession.request(method, 905 | url, 906 | json=params if (method in ["POST", "PUT"] and is_json) else None, 907 | data=params if (method in ["POST", "PUT"] and not is_json) else None, 908 | params=query_params, 909 | headers=headers, 910 | verify=not self.disable_ssl, 911 | allow_redirects=True, 912 | timeout=self.timeout, 913 | proxies=self.proxies) 914 | # Any requests lib related exceptions are raised here - https://requests.readthedocs.io/en/latest/api/#exceptions 915 | except Exception as e: 916 | raise e 917 | 918 | if self.debug: 919 | log.debug("Response: {code} {content}".format(code=r.status_code, content=r.content)) 920 | 921 | # Validate the content type. 922 | if "json" in r.headers["content-type"]: 923 | try: 924 | data = r.json() 925 | except ValueError: 926 | raise ex.DataException("Couldn't parse the JSON response received from the server: {content}".format( 927 | content=r.content)) 928 | 929 | # api error 930 | if data.get("status") == "error" or data.get("error_type"): 931 | # Call session hook if its registered and TokenException is raised 932 | if self.session_expiry_hook and r.status_code == 403 and data["error_type"] == "TokenException": 933 | self.session_expiry_hook() 934 | 935 | # native Kite errors 936 | exp = getattr(ex, data.get("error_type"), ex.GeneralException) 937 | raise exp(data["message"], code=r.status_code) 938 | 939 | return data["data"] 940 | elif "csv" in r.headers["content-type"]: 941 | return r.content 942 | else: 943 | raise ex.DataException("Unknown Content-Type ({content_type}) with response: ({content})".format( 944 | content_type=r.headers["content-type"], 945 | content=r.content)) 946 | -------------------------------------------------------------------------------- /kiteconnect/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | exceptions.py 4 | 5 | Exceptions raised by the Kite Connect client. 6 | 7 | :copyright: (c) 2021 by Zerodha Technology. 8 | :license: see LICENSE for details. 9 | """ 10 | 11 | 12 | class KiteException(Exception): 13 | """ 14 | Base exception class representing a Kite client exception. 15 | 16 | Every specific Kite client exception is a subclass of this 17 | and exposes two instance variables `.code` (HTTP error code) 18 | and `.message` (error text). 19 | """ 20 | 21 | def __init__(self, message, code=500): 22 | """Initialize the exception.""" 23 | super(KiteException, self).__init__(message) 24 | self.code = code 25 | 26 | 27 | class GeneralException(KiteException): 28 | """An unclassified, general error. Default code is 500.""" 29 | 30 | def __init__(self, message, code=500): 31 | """Initialize the exception.""" 32 | super(GeneralException, self).__init__(message, code) 33 | 34 | 35 | class TokenException(KiteException): 36 | """Represents all token and authentication related errors. Default code is 403.""" 37 | 38 | def __init__(self, message, code=403): 39 | """Initialize the exception.""" 40 | super(TokenException, self).__init__(message, code) 41 | 42 | 43 | class PermissionException(KiteException): 44 | """Represents permission denied exceptions for certain calls. Default code is 403.""" 45 | 46 | def __init__(self, message, code=403): 47 | """Initialize the exception.""" 48 | super(PermissionException, self).__init__(message, code) 49 | 50 | 51 | class OrderException(KiteException): 52 | """Represents all order placement and manipulation errors. Default code is 500.""" 53 | 54 | def __init__(self, message, code=500): 55 | """Initialize the exception.""" 56 | super(OrderException, self).__init__(message, code) 57 | 58 | 59 | class InputException(KiteException): 60 | """Represents user input errors such as missing and invalid parameters. Default code is 400.""" 61 | 62 | def __init__(self, message, code=400): 63 | """Initialize the exception.""" 64 | super(InputException, self).__init__(message, code) 65 | 66 | 67 | class DataException(KiteException): 68 | """Represents a bad response from the backend Order Management System (OMS). Default code is 502.""" 69 | 70 | def __init__(self, message, code=502): 71 | """Initialize the exception.""" 72 | super(DataException, self).__init__(message, code) 73 | 74 | 75 | class NetworkException(KiteException): 76 | """Represents a network issue between Kite and the backend Order Management System (OMS). Default code is 503.""" 77 | 78 | def __init__(self, message, code=503): 79 | """Initialize the exception.""" 80 | super(NetworkException, self).__init__(message, code) 81 | -------------------------------------------------------------------------------- /kiteconnect/ticker.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | ticker.py 4 | 5 | Websocket implementation for kite ticker 6 | 7 | :copyright: (c) 2021 by Zerodha Technology Pvt. Ltd. 8 | :license: see LICENSE for details. 9 | """ 10 | import six 11 | import sys 12 | import time 13 | import json 14 | import struct 15 | import logging 16 | import threading 17 | from datetime import datetime 18 | from twisted.internet import reactor, ssl 19 | from twisted.python import log as twisted_log 20 | from twisted.internet.protocol import ReconnectingClientFactory 21 | from autobahn.twisted.websocket import WebSocketClientProtocol, \ 22 | WebSocketClientFactory, connectWS 23 | 24 | from .__version__ import __version__, __title__ 25 | 26 | log = logging.getLogger(__name__) 27 | 28 | 29 | class KiteTickerClientProtocol(WebSocketClientProtocol): 30 | """Kite ticker autobahn WebSocket protocol.""" 31 | 32 | PING_INTERVAL = 2.5 33 | KEEPALIVE_INTERVAL = 5 34 | 35 | _next_ping = None 36 | _next_pong_check = None 37 | _last_pong_time = None 38 | _last_ping_time = None 39 | 40 | def __init__(self, *args, **kwargs): 41 | """Initialize protocol with all options passed from factory.""" 42 | super(KiteTickerClientProtocol, self).__init__(*args, **kwargs) 43 | 44 | # Overide method 45 | def onConnect(self, response): # noqa 46 | """Called when WebSocket server connection was established""" 47 | self.factory.ws = self 48 | 49 | if self.factory.on_connect: 50 | self.factory.on_connect(self, response) 51 | 52 | # Reset reconnect on successful reconnect 53 | self.factory.resetDelay() 54 | 55 | # Overide method 56 | def onOpen(self): # noqa 57 | """Called when the initial WebSocket opening handshake was completed.""" 58 | # send ping 59 | self._loop_ping() 60 | # init last pong check after X seconds 61 | self._loop_pong_check() 62 | 63 | if self.factory.on_open: 64 | self.factory.on_open(self) 65 | 66 | # Overide method 67 | def onMessage(self, payload, is_binary): # noqa 68 | """Called when text or binary message is received.""" 69 | if self.factory.on_message: 70 | self.factory.on_message(self, payload, is_binary) 71 | 72 | # Overide method 73 | def onClose(self, was_clean, code, reason): # noqa 74 | """Called when connection is closed.""" 75 | if not was_clean: 76 | if self.factory.on_error: 77 | self.factory.on_error(self, code, reason) 78 | 79 | if self.factory.on_close: 80 | self.factory.on_close(self, code, reason) 81 | 82 | # Cancel next ping and timer 83 | self._last_ping_time = None 84 | self._last_pong_time = None 85 | 86 | if self._next_ping: 87 | self._next_ping.cancel() 88 | 89 | if self._next_pong_check: 90 | self._next_pong_check.cancel() 91 | 92 | def onPong(self, response): # noqa 93 | """Called when pong message is received.""" 94 | if self._last_pong_time and self.factory.debug: 95 | log.debug("last pong was {} seconds back.".format(time.time() - self._last_pong_time)) 96 | 97 | self._last_pong_time = time.time() 98 | 99 | if self.factory.debug: 100 | log.debug("pong => {}".format(response)) 101 | 102 | """ 103 | Custom helper and exposed methods. 104 | """ 105 | 106 | def _loop_ping(self): # noqa 107 | """Start a ping loop where it sends ping message every X seconds.""" 108 | if self.factory.debug: 109 | if self._last_ping_time: 110 | log.debug("last ping was {} seconds back.".format(time.time() - self._last_ping_time)) 111 | 112 | # Set current time as last ping time 113 | self._last_ping_time = time.time() 114 | 115 | # Call self after X seconds 116 | self._next_ping = self.factory.reactor.callLater(self.PING_INTERVAL, self._loop_ping) 117 | 118 | def _loop_pong_check(self): 119 | """ 120 | Timer sortof to check if connection is still there. 121 | 122 | Checks last pong message time and disconnects the existing connection to make sure it doesn't become a ghost connection. 123 | """ 124 | if self._last_pong_time: 125 | # No pong message since long time, so init reconnect 126 | last_pong_diff = time.time() - self._last_pong_time 127 | if last_pong_diff > (2 * self.PING_INTERVAL): 128 | if self.factory.debug: 129 | log.debug("Last pong was {} seconds ago. So dropping connection to reconnect.".format( 130 | last_pong_diff)) 131 | # drop existing connection to avoid ghost connection 132 | self.dropConnection(abort=True) 133 | 134 | # Call self after X seconds 135 | self._next_pong_check = self.factory.reactor.callLater(self.PING_INTERVAL, self._loop_pong_check) 136 | 137 | 138 | class KiteTickerClientFactory(WebSocketClientFactory, ReconnectingClientFactory): 139 | """Autobahn WebSocket client factory to implement reconnection and custom callbacks.""" 140 | 141 | protocol = KiteTickerClientProtocol 142 | maxDelay = 5 143 | maxRetries = 10 144 | 145 | _last_connection_time = None 146 | 147 | def __init__(self, *args, **kwargs): 148 | """Initialize with default callback method values.""" 149 | self.debug = False 150 | self.ws = None 151 | self.on_open = None 152 | self.on_error = None 153 | self.on_close = None 154 | self.on_message = None 155 | self.on_connect = None 156 | self.on_reconnect = None 157 | self.on_noreconnect = None 158 | 159 | super(KiteTickerClientFactory, self).__init__(*args, **kwargs) 160 | 161 | def startedConnecting(self, connector): # noqa 162 | """On connecting start or reconnection.""" 163 | if not self._last_connection_time and self.debug: 164 | log.debug("Start WebSocket connection.") 165 | 166 | self._last_connection_time = time.time() 167 | 168 | def clientConnectionFailed(self, connector, reason): # noqa 169 | """On connection failure (When connect request fails)""" 170 | if self.retries > 0: 171 | log.error("Retrying connection. Retry attempt count: {}. Next retry in around: {} seconds".format(self.retries, int(round(self.delay)))) 172 | 173 | # on reconnect callback 174 | if self.on_reconnect: 175 | self.on_reconnect(self.retries) 176 | 177 | # Retry the connection 178 | self.retry(connector) 179 | self.send_noreconnect() 180 | 181 | def clientConnectionLost(self, connector, reason): # noqa 182 | """On connection lost (When ongoing connection got disconnected).""" 183 | if self.retries > 0: 184 | # on reconnect callback 185 | if self.on_reconnect: 186 | self.on_reconnect(self.retries) 187 | 188 | # Retry the connection 189 | self.retry(connector) 190 | self.send_noreconnect() 191 | 192 | def send_noreconnect(self): 193 | """Callback `no_reconnect` if max retries are exhausted.""" 194 | if self.maxRetries is not None and (self.retries > self.maxRetries): 195 | if self.debug: 196 | log.debug("Maximum retries ({}) exhausted.".format(self.maxRetries)) 197 | # Stop the loop for exceeding max retry attempts 198 | self.stop() 199 | 200 | if self.on_noreconnect: 201 | self.on_noreconnect() 202 | 203 | 204 | class KiteTicker(object): 205 | """ 206 | The WebSocket client for connecting to Kite Connect's streaming quotes service. 207 | 208 | Getting started: 209 | --------------- 210 | #!python 211 | import logging 212 | from kiteconnect import KiteTicker 213 | 214 | logging.basicConfig(level=logging.DEBUG) 215 | 216 | # Initialise 217 | kws = KiteTicker("your_api_key", "your_access_token") 218 | 219 | def on_ticks(ws, ticks): 220 | # Callback to receive ticks. 221 | logging.debug("Ticks: {}".format(ticks)) 222 | 223 | def on_connect(ws, response): 224 | # Callback on successful connect. 225 | # Subscribe to a list of instrument_tokens (RELIANCE and ACC here). 226 | ws.subscribe([738561, 5633]) 227 | 228 | # Set RELIANCE to tick in `full` mode. 229 | ws.set_mode(ws.MODE_FULL, [738561]) 230 | 231 | def on_close(ws, code, reason): 232 | # On connection close stop the event loop. 233 | # Reconnection will not happen after executing `ws.stop()` 234 | ws.stop() 235 | 236 | # Assign the callbacks. 237 | kws.on_ticks = on_ticks 238 | kws.on_connect = on_connect 239 | kws.on_close = on_close 240 | 241 | # Infinite loop on the main thread. Nothing after this will run. 242 | # You have to use the pre-defined callbacks to manage subscriptions. 243 | kws.connect() 244 | 245 | Callbacks 246 | --------- 247 | In below examples `ws` is the currently initialised WebSocket object. 248 | 249 | - `on_ticks(ws, ticks)` - Triggered when ticks are recevied. 250 | - `ticks` - List of `tick` object. Check below for sample structure. 251 | - `on_close(ws, code, reason)` - Triggered when connection is closed. 252 | - `code` - WebSocket standard close event code (https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent) 253 | - `reason` - DOMString indicating the reason the server closed the connection 254 | - `on_error(ws, code, reason)` - Triggered when connection is closed with an error. 255 | - `code` - WebSocket standard close event code (https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent) 256 | - `reason` - DOMString indicating the reason the server closed the connection 257 | - `on_connect` - Triggered when connection is established successfully. 258 | - `response` - Response received from server on successful connection. 259 | - `on_message(ws, payload, is_binary)` - Triggered when message is received from the server. 260 | - `payload` - Raw response from the server (either text or binary). 261 | - `is_binary` - Bool to check if response is binary type. 262 | - `on_reconnect(ws, attempts_count)` - Triggered when auto reconnection is attempted. 263 | - `attempts_count` - Current reconnect attempt number. 264 | - `on_noreconnect(ws)` - Triggered when number of auto reconnection attempts exceeds `reconnect_tries`. 265 | - `on_order_update(ws, data)` - Triggered when there is an order update for the connected user. 266 | 267 | 268 | Tick structure (passed to the `on_ticks` callback) 269 | --------------------------- 270 | [{ 271 | 'instrument_token': 53490439, 272 | 'mode': 'full', 273 | 'volume_traded': 12510, 274 | 'last_price': 4084.0, 275 | 'average_traded_price': 4086.55, 276 | 'last_traded_quantity': 1, 277 | 'total_buy_quantity': 2356 278 | 'total_sell_quantity': 2440, 279 | 'change': 0.46740467404674046, 280 | 'last_trade_time': datetime.datetime(2018, 1, 15, 13, 16, 54), 281 | 'exchange_timestamp': datetime.datetime(2018, 1, 15, 13, 16, 56), 282 | 'oi': 21845, 283 | 'oi_day_low': 0, 284 | 'oi_day_high': 0, 285 | 'ohlc': { 286 | 'high': 4093.0, 287 | 'close': 4065.0, 288 | 'open': 4088.0, 289 | 'low': 4080.0 290 | }, 291 | 'tradable': True, 292 | 'depth': { 293 | 'sell': [{ 294 | 'price': 4085.0, 295 | 'orders': 1048576, 296 | 'quantity': 43 297 | }, { 298 | 'price': 4086.0, 299 | 'orders': 2752512, 300 | 'quantity': 134 301 | }, { 302 | 'price': 4087.0, 303 | 'orders': 1703936, 304 | 'quantity': 133 305 | }, { 306 | 'price': 4088.0, 307 | 'orders': 1376256, 308 | 'quantity': 70 309 | }, { 310 | 'price': 4089.0, 311 | 'orders': 1048576, 312 | 'quantity': 46 313 | }], 314 | 'buy': [{ 315 | 'price': 4084.0, 316 | 'orders': 589824, 317 | 'quantity': 53 318 | }, { 319 | 'price': 4083.0, 320 | 'orders': 1245184, 321 | 'quantity': 145 322 | }, { 323 | 'price': 4082.0, 324 | 'orders': 1114112, 325 | 'quantity': 63 326 | }, { 327 | 'price': 4081.0, 328 | 'orders': 1835008, 329 | 'quantity': 69 330 | }, { 331 | 'price': 4080.0, 332 | 'orders': 2752512, 333 | 'quantity': 89 334 | }] 335 | } 336 | }, 337 | ..., 338 | ...] 339 | 340 | Auto reconnection 341 | ----------------- 342 | 343 | Auto reconnection is enabled by default and it can be disabled by passing `reconnect` param while initialising `KiteTicker`. 344 | On a side note, reconnection mechanism cannot happen if event loop is terminated using `stop` method inside `on_close` callback. 345 | 346 | Auto reonnection mechanism is based on [Exponential backoff](https://en.wikipedia.org/wiki/Exponential_backoff) algorithm in which 347 | next retry interval will be increased exponentially. `reconnect_max_delay` and `reconnect_max_tries` params can be used to tewak 348 | the alogrithm where `reconnect_max_delay` is the maximum delay after which subsequent reconnection interval will become constant and 349 | `reconnect_max_tries` is maximum number of retries before its quiting reconnection. 350 | 351 | For example if `reconnect_max_delay` is 60 seconds and `reconnect_max_tries` is 50 then the first reconnection interval starts from 352 | minimum interval which is 2 seconds and keep increasing up to 60 seconds after which it becomes constant and when reconnection attempt 353 | is reached upto 50 then it stops reconnecting. 354 | 355 | method `stop_retry` can be used to stop ongoing reconnect attempts and `on_reconnect` callback will be called with current reconnect 356 | attempt and `on_noreconnect` is called when reconnection attempts reaches max retries. 357 | """ 358 | 359 | EXCHANGE_MAP = { 360 | "nse": 1, 361 | "nfo": 2, 362 | "cds": 3, 363 | "bse": 4, 364 | "bfo": 5, 365 | "bcd": 6, 366 | "mcx": 7, 367 | "mcxsx": 8, 368 | "indices": 9, 369 | # bsecds is replaced with it's official segment name bcd 370 | # so,bsecds key will be depreciated in next version 371 | "bsecds": 6, 372 | } 373 | 374 | # Default connection timeout 375 | CONNECT_TIMEOUT = 30 376 | # Default Reconnect max delay. 377 | RECONNECT_MAX_DELAY = 60 378 | # Default reconnect attempts 379 | RECONNECT_MAX_TRIES = 50 380 | # Default root API endpoint. It's possible to 381 | # override this by passing the `root` parameter during initialisation. 382 | ROOT_URI = "wss://ws.kite.trade" 383 | 384 | # Available streaming modes. 385 | MODE_FULL = "full" 386 | MODE_QUOTE = "quote" 387 | MODE_LTP = "ltp" 388 | 389 | # Flag to set if its first connect 390 | _is_first_connect = True 391 | 392 | # Available actions. 393 | _message_code = 11 394 | _message_subscribe = "subscribe" 395 | _message_unsubscribe = "unsubscribe" 396 | _message_setmode = "mode" 397 | 398 | # Minimum delay which should be set between retries. User can't set less than this 399 | _minimum_reconnect_max_delay = 5 400 | # Maximum number or retries user can set 401 | _maximum_reconnect_max_tries = 300 402 | 403 | def __init__(self, api_key, access_token, debug=False, root=None, 404 | reconnect=True, reconnect_max_tries=RECONNECT_MAX_TRIES, reconnect_max_delay=RECONNECT_MAX_DELAY, 405 | connect_timeout=CONNECT_TIMEOUT): 406 | """ 407 | Initialise websocket client instance. 408 | 409 | - `api_key` is the API key issued to you 410 | - `access_token` is the token obtained after the login flow in 411 | exchange for the `request_token`. Pre-login, this will default to None, 412 | but once you have obtained it, you should 413 | persist it in a database or session to pass 414 | to the Kite Connect class initialisation for subsequent requests. 415 | - `root` is the websocket API end point root. Unless you explicitly 416 | want to send API requests to a non-default endpoint, this 417 | can be ignored. 418 | - `reconnect` is a boolean to enable WebSocket autreconnect in case of network failure/disconnection. 419 | - `reconnect_max_delay` in seconds is the maximum delay after which subsequent reconnection interval will become constant. Defaults to 60s and minimum acceptable value is 5s. 420 | - `reconnect_max_tries` is maximum number reconnection attempts. Defaults to 50 attempts and maximum up to 300 attempts. 421 | - `connect_timeout` in seconds is the maximum interval after which connection is considered as timeout. Defaults to 30s. 422 | """ 423 | self.root = root or self.ROOT_URI 424 | 425 | # Set max reconnect tries 426 | if reconnect_max_tries > self._maximum_reconnect_max_tries: 427 | log.warning("`reconnect_max_tries` can not be more than {val}. Setting to highest possible value - {val}.".format( 428 | val=self._maximum_reconnect_max_tries)) 429 | self.reconnect_max_tries = self._maximum_reconnect_max_tries 430 | else: 431 | self.reconnect_max_tries = reconnect_max_tries 432 | 433 | # Set max reconnect delay 434 | if reconnect_max_delay < self._minimum_reconnect_max_delay: 435 | log.warning("`reconnect_max_delay` can not be less than {val}. Setting to lowest possible value - {val}.".format( 436 | val=self._minimum_reconnect_max_delay)) 437 | self.reconnect_max_delay = self._minimum_reconnect_max_delay 438 | else: 439 | self.reconnect_max_delay = reconnect_max_delay 440 | 441 | self.connect_timeout = connect_timeout 442 | 443 | self.socket_url = "{root}?api_key={api_key}"\ 444 | "&access_token={access_token}".format( 445 | root=self.root, 446 | api_key=api_key, 447 | access_token=access_token 448 | ) 449 | 450 | # Debug enables logs 451 | self.debug = debug 452 | 453 | # Initialize default value for websocket object 454 | self.ws = None 455 | 456 | # Placeholders for callbacks. 457 | self.on_ticks = None 458 | self.on_open = None 459 | self.on_close = None 460 | self.on_error = None 461 | self.on_connect = None 462 | self.on_message = None 463 | self.on_reconnect = None 464 | self.on_noreconnect = None 465 | 466 | # Text message updates 467 | self.on_order_update = None 468 | 469 | # List of current subscribed tokens 470 | self.subscribed_tokens = {} 471 | 472 | def _create_connection(self, url, **kwargs): 473 | """Create a WebSocket client connection.""" 474 | self.factory = KiteTickerClientFactory(url, **kwargs) 475 | 476 | # Alias for current websocket connection 477 | self.ws = self.factory.ws 478 | 479 | self.factory.debug = self.debug 480 | 481 | # Register private callbacks 482 | self.factory.on_open = self._on_open 483 | self.factory.on_error = self._on_error 484 | self.factory.on_close = self._on_close 485 | self.factory.on_message = self._on_message 486 | self.factory.on_connect = self._on_connect 487 | self.factory.on_reconnect = self._on_reconnect 488 | self.factory.on_noreconnect = self._on_noreconnect 489 | 490 | self.factory.maxDelay = self.reconnect_max_delay 491 | self.factory.maxRetries = self.reconnect_max_tries 492 | 493 | def _user_agent(self): 494 | return (__title__ + "-python/").capitalize() + __version__ 495 | 496 | def connect(self, threaded=False, disable_ssl_verification=False, proxy=None): 497 | """ 498 | Establish a websocket connection. 499 | 500 | - `threaded` is a boolean indicating if the websocket client has to be run in threaded mode or not 501 | - `disable_ssl_verification` disables building ssl context 502 | - `proxy` is a dictionary with keys `host` and `port` which denotes the proxy settings 503 | """ 504 | # Custom headers 505 | headers = { 506 | "X-Kite-Version": "3", # For version 3 507 | } 508 | 509 | # Init WebSocket client factory 510 | self._create_connection(self.socket_url, 511 | useragent=self._user_agent(), 512 | proxy=proxy, headers=headers) 513 | 514 | # Set SSL context 515 | context_factory = None 516 | if self.factory.isSecure and not disable_ssl_verification: 517 | context_factory = ssl.ClientContextFactory() 518 | 519 | # Establish WebSocket connection to a server 520 | connectWS(self.factory, contextFactory=context_factory, timeout=self.connect_timeout) 521 | 522 | if self.debug: 523 | twisted_log.startLogging(sys.stdout) 524 | 525 | # Run in seperate thread of blocking 526 | opts = {} 527 | 528 | # Run when reactor is not running 529 | if not reactor.running: 530 | if threaded: 531 | # Signals are not allowed in non main thread by twisted so suppress it. 532 | opts["installSignalHandlers"] = False 533 | self.websocket_thread = threading.Thread(target=reactor.run, kwargs=opts) 534 | self.websocket_thread.daemon = True 535 | self.websocket_thread.start() 536 | else: 537 | reactor.run(**opts) 538 | 539 | def is_connected(self): 540 | """Check if WebSocket connection is established.""" 541 | if self.ws and self.ws.state == self.ws.STATE_OPEN: 542 | return True 543 | else: 544 | return False 545 | 546 | def _close(self, code=None, reason=None): 547 | """Close the WebSocket connection.""" 548 | if self.ws: 549 | self.ws.sendClose(code, reason) 550 | 551 | def close(self, code=None, reason=None): 552 | """Close the WebSocket connection.""" 553 | self.stop_retry() 554 | self._close(code, reason) 555 | 556 | def stop(self): 557 | """Stop the event loop. Should be used if main thread has to be closed in `on_close` method. 558 | Reconnection mechanism cannot happen past this method 559 | """ 560 | reactor.stop() 561 | 562 | def stop_retry(self): 563 | """Stop auto retry when it is in progress.""" 564 | if self.factory: 565 | self.factory.stopTrying() 566 | 567 | def subscribe(self, instrument_tokens): 568 | """ 569 | Subscribe to a list of instrument_tokens. 570 | 571 | - `instrument_tokens` is list of instrument instrument_tokens to subscribe 572 | """ 573 | try: 574 | self.ws.sendMessage( 575 | six.b(json.dumps({"a": self._message_subscribe, "v": instrument_tokens})) 576 | ) 577 | 578 | for token in instrument_tokens: 579 | self.subscribed_tokens[token] = self.MODE_QUOTE 580 | 581 | return True 582 | except Exception as e: 583 | self._close(reason="Error while subscribe: {}".format(str(e))) 584 | raise 585 | 586 | def unsubscribe(self, instrument_tokens): 587 | """ 588 | Unsubscribe the given list of instrument_tokens. 589 | 590 | - `instrument_tokens` is list of instrument_tokens to unsubscribe. 591 | """ 592 | try: 593 | self.ws.sendMessage( 594 | six.b(json.dumps({"a": self._message_unsubscribe, "v": instrument_tokens})) 595 | ) 596 | 597 | for token in instrument_tokens: 598 | try: 599 | del (self.subscribed_tokens[token]) 600 | except KeyError: 601 | pass 602 | 603 | return True 604 | except Exception as e: 605 | self._close(reason="Error while unsubscribe: {}".format(str(e))) 606 | raise 607 | 608 | def set_mode(self, mode, instrument_tokens): 609 | """ 610 | Set streaming mode for the given list of tokens. 611 | 612 | - `mode` is the mode to set. It can be one of the following class constants: 613 | MODE_LTP, MODE_QUOTE, or MODE_FULL. 614 | - `instrument_tokens` is list of instrument tokens on which the mode should be applied 615 | """ 616 | try: 617 | self.ws.sendMessage( 618 | six.b(json.dumps({"a": self._message_setmode, "v": [mode, instrument_tokens]})) 619 | ) 620 | 621 | # Update modes 622 | for token in instrument_tokens: 623 | self.subscribed_tokens[token] = mode 624 | 625 | return True 626 | except Exception as e: 627 | self._close(reason="Error while setting mode: {}".format(str(e))) 628 | raise 629 | 630 | def resubscribe(self): 631 | """Resubscribe to all current subscribed tokens.""" 632 | modes = {} 633 | 634 | for token in self.subscribed_tokens: 635 | m = self.subscribed_tokens[token] 636 | 637 | if not modes.get(m): 638 | modes[m] = [] 639 | 640 | modes[m].append(token) 641 | 642 | for mode in modes: 643 | if self.debug: 644 | log.debug("Resubscribe and set mode: {} - {}".format(mode, modes[mode])) 645 | 646 | self.subscribe(modes[mode]) 647 | self.set_mode(mode, modes[mode]) 648 | 649 | def _on_connect(self, ws, response): 650 | self.ws = ws 651 | if self.on_connect: 652 | self.on_connect(self, response) 653 | 654 | def _on_close(self, ws, code, reason): 655 | """Call `on_close` callback when connection is closed.""" 656 | log.error("Connection closed: {} - {}".format(code, str(reason))) 657 | 658 | if self.on_close: 659 | self.on_close(self, code, reason) 660 | 661 | def _on_error(self, ws, code, reason): 662 | """Call `on_error` callback when connection throws an error.""" 663 | log.error("Connection error: {} - {}".format(code, str(reason))) 664 | 665 | if self.on_error: 666 | self.on_error(self, code, reason) 667 | 668 | def _on_message(self, ws, payload, is_binary): 669 | """Call `on_message` callback when text message is received.""" 670 | if self.on_message: 671 | self.on_message(self, payload, is_binary) 672 | 673 | # If the message is binary, parse it and send it to the callback. 674 | if self.on_ticks and is_binary and len(payload) > 4: 675 | self.on_ticks(self, self._parse_binary(payload)) 676 | 677 | # Parse text messages 678 | if not is_binary: 679 | self._parse_text_message(payload) 680 | 681 | def _on_open(self, ws): 682 | # Resubscribe if its reconnect 683 | if not self._is_first_connect: 684 | self.resubscribe() 685 | 686 | # Set first connect to false once its connected first time 687 | self._is_first_connect = False 688 | 689 | if self.on_open: 690 | return self.on_open(self) 691 | 692 | def _on_reconnect(self, attempts_count): 693 | if self.on_reconnect: 694 | return self.on_reconnect(self, attempts_count) 695 | 696 | def _on_noreconnect(self): 697 | if self.on_noreconnect: 698 | return self.on_noreconnect(self) 699 | 700 | def _parse_text_message(self, payload): 701 | """Parse text message.""" 702 | # Decode unicode data 703 | if not six.PY2 and type(payload) == bytes: 704 | payload = payload.decode("utf-8") 705 | 706 | try: 707 | data = json.loads(payload) 708 | except ValueError: 709 | return 710 | 711 | # Order update callback 712 | if self.on_order_update and data.get("type") == "order" and data.get("data"): 713 | self.on_order_update(self, data["data"]) 714 | 715 | # Custom error with websocket error code 0 716 | if data.get("type") == "error": 717 | self._on_error(self, 0, data.get("data")) 718 | 719 | def _parse_binary(self, bin): 720 | """Parse binary data to a (list of) ticks structure.""" 721 | packets = self._split_packets(bin) # split data to individual ticks packet 722 | data = [] 723 | 724 | for packet in packets: 725 | instrument_token = self._unpack_int(packet, 0, 4) 726 | segment = instrument_token & 0xff # Retrive segment constant from instrument_token 727 | 728 | # Add price divisor based on segment 729 | if segment == self.EXCHANGE_MAP["cds"]: 730 | divisor = 10000000.0 731 | elif segment == self.EXCHANGE_MAP["bcd"]: 732 | divisor = 10000.0 733 | else: 734 | divisor = 100.0 735 | 736 | # All indices are not tradable 737 | tradable = False if segment == self.EXCHANGE_MAP["indices"] else True 738 | 739 | # LTP packets 740 | if len(packet) == 8: 741 | data.append({ 742 | "tradable": tradable, 743 | "mode": self.MODE_LTP, 744 | "instrument_token": instrument_token, 745 | "last_price": self._unpack_int(packet, 4, 8) / divisor 746 | }) 747 | # Indices quote and full mode 748 | elif len(packet) == 28 or len(packet) == 32: 749 | mode = self.MODE_QUOTE if len(packet) == 28 else self.MODE_FULL 750 | 751 | d = { 752 | "tradable": tradable, 753 | "mode": mode, 754 | "instrument_token": instrument_token, 755 | "last_price": self._unpack_int(packet, 4, 8) / divisor, 756 | "ohlc": { 757 | "high": self._unpack_int(packet, 8, 12) / divisor, 758 | "low": self._unpack_int(packet, 12, 16) / divisor, 759 | "open": self._unpack_int(packet, 16, 20) / divisor, 760 | "close": self._unpack_int(packet, 20, 24) / divisor 761 | } 762 | } 763 | 764 | # Compute the change price using close price and last price 765 | d["change"] = 0 766 | if (d["ohlc"]["close"] != 0): 767 | d["change"] = (d["last_price"] - d["ohlc"]["close"]) * 100 / d["ohlc"]["close"] 768 | 769 | # Full mode with timestamp 770 | if len(packet) == 32: 771 | try: 772 | timestamp = datetime.fromtimestamp(self._unpack_int(packet, 28, 32)) 773 | except Exception: 774 | timestamp = None 775 | 776 | d["exchange_timestamp"] = timestamp 777 | 778 | data.append(d) 779 | # Quote and full mode 780 | elif len(packet) == 44 or len(packet) == 184: 781 | mode = self.MODE_QUOTE if len(packet) == 44 else self.MODE_FULL 782 | 783 | d = { 784 | "tradable": tradable, 785 | "mode": mode, 786 | "instrument_token": instrument_token, 787 | "last_price": self._unpack_int(packet, 4, 8) / divisor, 788 | "last_traded_quantity": self._unpack_int(packet, 8, 12), 789 | "average_traded_price": self._unpack_int(packet, 12, 16) / divisor, 790 | "volume_traded": self._unpack_int(packet, 16, 20), 791 | "total_buy_quantity": self._unpack_int(packet, 20, 24), 792 | "total_sell_quantity": self._unpack_int(packet, 24, 28), 793 | "ohlc": { 794 | "open": self._unpack_int(packet, 28, 32) / divisor, 795 | "high": self._unpack_int(packet, 32, 36) / divisor, 796 | "low": self._unpack_int(packet, 36, 40) / divisor, 797 | "close": self._unpack_int(packet, 40, 44) / divisor 798 | } 799 | } 800 | 801 | # Compute the change price using close price and last price 802 | d["change"] = 0 803 | if (d["ohlc"]["close"] != 0): 804 | d["change"] = (d["last_price"] - d["ohlc"]["close"]) * 100 / d["ohlc"]["close"] 805 | 806 | # Parse full mode 807 | if len(packet) == 184: 808 | try: 809 | last_trade_time = datetime.fromtimestamp(self._unpack_int(packet, 44, 48)) 810 | except Exception: 811 | last_trade_time = None 812 | 813 | try: 814 | timestamp = datetime.fromtimestamp(self._unpack_int(packet, 60, 64)) 815 | except Exception: 816 | timestamp = None 817 | 818 | d["last_trade_time"] = last_trade_time 819 | d["oi"] = self._unpack_int(packet, 48, 52) 820 | d["oi_day_high"] = self._unpack_int(packet, 52, 56) 821 | d["oi_day_low"] = self._unpack_int(packet, 56, 60) 822 | d["exchange_timestamp"] = timestamp 823 | 824 | # Market depth entries. 825 | depth = { 826 | "buy": [], 827 | "sell": [] 828 | } 829 | 830 | # Compile the market depth lists. 831 | for i, p in enumerate(range(64, len(packet), 12)): 832 | depth["sell" if i >= 5 else "buy"].append({ 833 | "quantity": self._unpack_int(packet, p, p + 4), 834 | "price": self._unpack_int(packet, p + 4, p + 8) / divisor, 835 | "orders": self._unpack_int(packet, p + 8, p + 10, byte_format="H") 836 | }) 837 | 838 | d["depth"] = depth 839 | 840 | data.append(d) 841 | 842 | return data 843 | 844 | def _unpack_int(self, bin, start, end, byte_format="I"): 845 | """Unpack binary data as unsgined interger.""" 846 | return struct.unpack(">" + byte_format, bin[start:end])[0] 847 | 848 | def _split_packets(self, bin): 849 | """Split the data to individual packets of ticks.""" 850 | # Ignore heartbeat data. 851 | if len(bin) < 2: 852 | return [] 853 | 854 | number_of_packets = self._unpack_int(bin, 0, 2, byte_format="H") 855 | packets = [] 856 | 857 | j = 2 858 | for i in range(number_of_packets): 859 | packet_length = self._unpack_int(bin, j, j + 2, byte_format="H") 860 | packets.append(bin[j + 2: j + 2 + packet_length]) 861 | j = j + 2 + packet_length 862 | 863 | return packets 864 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = tests/unit 3 | addopts = 4 | -r fEsxXw 5 | -vvv 6 | --doctest-modules 7 | --ignore setup.py 8 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude=build,.git,.eggs,examples,tests 3 | ignore=D100,D401,E501 4 | max-line-length = 119 5 | 6 | [aliases] 7 | test = pytest 8 | 9 | [tool:pytest] 10 | norecursedirs=tests/helpers tests/mock_responses 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import io 4 | import os 5 | from codecs import open 6 | from setuptools import setup 7 | 8 | current_dir = os.path.abspath(os.path.dirname(__file__)) 9 | 10 | about = {} 11 | with open(os.path.join(current_dir, "kiteconnect", "__version__.py"), "r", "utf-8") as f: 12 | exec(f.read(), about) 13 | 14 | with io.open('README.md', 'rt', encoding='utf8') as f: 15 | readme = f.read() 16 | 17 | setup( 18 | name=about["__title__"], 19 | version=about["__version__"], 20 | description=about["__description__"], 21 | long_description=readme, 22 | long_description_content_type='text/markdown', 23 | author=about["__author__"], 24 | author_email=about["__author_email__"], 25 | url=about["__url__"], 26 | download_url=about["__download_url__"], 27 | license=about["__license__"], 28 | packages=["kiteconnect"], 29 | classifiers=[ 30 | "Development Status :: 5 - Production/Stable", 31 | "Intended Audience :: Developers", 32 | "Intended Audience :: Financial and Insurance Industry", 33 | "Programming Language :: Python", 34 | "Natural Language :: English", 35 | "License :: OSI Approved :: MIT License", 36 | "Programming Language :: Python :: 3.5", 37 | "Programming Language :: Python :: 3.6", 38 | "Programming Language :: Python :: 3.7", 39 | "Topic :: Office/Business :: Financial :: Investment", 40 | "Topic :: Software Development :: Libraries :: Python Modules", 41 | "Topic :: Software Development :: Libraries" 42 | ], 43 | install_requires=[ 44 | "service_identity>=18.1.0", 45 | "requests>=2.18.4", 46 | "six>=1.11.0", 47 | "pyOpenSSL>=17.5.0", 48 | "python-dateutil>=2.6.1", 49 | "autobahn[twisted]==19.11.2" 50 | ], 51 | tests_require=["pytest", "responses", "pytest-cov", "mock", "flake8"], 52 | test_suite="tests", 53 | setup_requires=["pytest-runner"], 54 | extras_require={ 55 | "doc": ["pdoc"], 56 | ':sys_platform=="win32"': ["pywin32"] 57 | } 58 | ) 59 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zerodha/pykiteconnect/6b7b7621e575411921b506203b526bf275a702c7/tests/__init__.py -------------------------------------------------------------------------------- /tests/helpers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zerodha/pykiteconnect/6b7b7621e575411921b506203b526bf275a702c7/tests/helpers/__init__.py -------------------------------------------------------------------------------- /tests/helpers/utils.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import os 3 | import json 4 | 5 | # Mock responses path 6 | responses_path = { 7 | "base": "../mock_responses/", 8 | "user.profile": "profile.json", 9 | "user.margins": "margins.json", 10 | "user.margins.segment": "margins.json", 11 | 12 | "orders": "orders.json", 13 | "trades": "trades.json", 14 | "order.info": "order_info.json", 15 | "order.trades": "order_trades.json", 16 | 17 | "portfolio.positions": "positions.json", 18 | "portfolio.holdings": "holdings.json", 19 | "portfolio.holdings.auction": "auctions_list.json", 20 | 21 | # MF api endpoints 22 | "mf.orders": "mf_orders.json", 23 | "mf.order.info": "mf_orders_info.json", 24 | 25 | "mf.sips": "mf_sips.json", 26 | "mf.sip.info": "mf_sip_info.json", 27 | 28 | "mf.holdings": "mf_holdings.json", 29 | "mf.instruments": "mf_instruments.csv", 30 | 31 | "market.instruments": "instruments_nse.csv", 32 | "market.instruments.all": "instruments_all.csv", 33 | "market.historical": "historical_minute.json", 34 | "market.trigger_range": "trigger_range.json", 35 | 36 | "market.quote": "quote.json", 37 | "market.quote.ohlc": "ohlc.json", 38 | "market.quote.ltp": "ltp.json", 39 | 40 | "gtt": "gtt_get_orders.json", 41 | "gtt.place": "gtt_place_order.json", 42 | "gtt.info": "gtt_get_order.json", 43 | "gtt.modify": "gtt_modify_order.json", 44 | "gtt.delete": "gtt_delete_order.json", 45 | 46 | # Order margin & charges 47 | "order.margins": "order_margins.json", 48 | "order.margins.basket": "basket_margins.json", 49 | "order.contract_note": "virtual_contract_note.json" 50 | } 51 | 52 | 53 | def full_path(rel_path): 54 | """return the full path of given rel_path.""" 55 | return os.path.abspath( 56 | os.path.join( 57 | os.path.dirname(__file__), 58 | rel_path 59 | ) 60 | ) 61 | 62 | 63 | def get_response(key): 64 | """Get mock response based on route.""" 65 | path = full_path(responses_path["base"] + responses_path[key]) 66 | return open(path, "r").read() 67 | 68 | 69 | def get_json_response(key): 70 | """Get json mock response based on route.""" 71 | return json.loads(get_response(key)) 72 | 73 | 74 | def assert_responses(inp, sample): 75 | """Check if all keys given as a list is there in input.""" 76 | # Type check only if its a list or dict 77 | # Issue with checking all types are a float value can be inferred as int and 78 | # in some responses it will None instrad of empty string 79 | if type(sample) in [list, dict]: 80 | assert type(inp) == type(sample) 81 | 82 | # If its a list then just check the first element if its available 83 | if type(inp) == list and len(inp) > 0: 84 | assert_responses(inp[0], sample[0]) 85 | 86 | # If its a dict then iterate individual keys to test 87 | if type(sample) == dict: 88 | for key in sample.keys(): 89 | assert_responses(inp[key], sample[key]) 90 | 91 | 92 | def merge_dicts(x, y): 93 | z = x.copy() 94 | z.update(y) 95 | return z 96 | -------------------------------------------------------------------------------- /tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zerodha/pykiteconnect/6b7b7621e575411921b506203b526bf275a702c7/tests/integration/__init__.py -------------------------------------------------------------------------------- /tests/integration/conftest.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """Pytest config.""" 4 | import os 5 | import sys 6 | import pytest 7 | from kiteconnect import KiteConnect 8 | 9 | sys.path.append(os.path.join(os.path.dirname(__file__), '../helpers')) 10 | 11 | 12 | def pytest_addoption(parser): 13 | """Add available args.""" 14 | parser.addoption("--api-key", action="store", default="Api key") 15 | parser.addoption("--access-token", action="store", default="Access token") 16 | parser.addoption("--root-url", action="store", default="") 17 | 18 | 19 | def pytest_generate_tests(metafunc): 20 | """This is called for every test. Only get/set command line arguments. If the argument is specified in the list of test "fixturenames".""" 21 | access_token = metafunc.config.option.access_token 22 | api_key = metafunc.config.option.api_key 23 | root_url = metafunc.config.option.root_url 24 | 25 | if "access_token" in metafunc.fixturenames and access_token is not None: 26 | metafunc.parametrize("access_token", [access_token]) 27 | 28 | if "api_key" in metafunc.fixturenames and api_key is not None: 29 | metafunc.parametrize("api_key", [api_key]) 30 | 31 | if "root_url" in metafunc.fixturenames and root_url is not None: 32 | metafunc.parametrize("root_url", [root_url]) 33 | 34 | 35 | @pytest.fixture() 36 | def kiteconnect(api_key, access_token, root_url): 37 | """Init Kite connect object.""" 38 | return KiteConnect(api_key=api_key, access_token=access_token, root=root_url or None) 39 | -------------------------------------------------------------------------------- /tests/integration/test_connect_read.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import pytest 4 | from mock import Mock 5 | import utils 6 | import time 7 | import datetime 8 | import warnings 9 | import kiteconnect.exceptions as ex 10 | 11 | 12 | def test_request_pool(): 13 | from kiteconnect import KiteConnect 14 | pool = { 15 | "pool_connections": 10, 16 | "pool_maxsize": 10, 17 | "max_retries": 0, 18 | "pool_block": False 19 | } 20 | 21 | kiteconnect = KiteConnect(api_key="random", access_token="random", pool=pool) 22 | 23 | with pytest.raises(ex.TokenException): 24 | kiteconnect.orders() 25 | 26 | 27 | def test_set_access_token(kiteconnect): 28 | """Check for token exception when invalid token is set.""" 29 | kiteconnect.set_access_token("invalid_token") 30 | with pytest.raises(ex.TokenException): 31 | kiteconnect.positions() 32 | 33 | 34 | def test_set_session_expiry_hook(kiteconnect): 35 | """Test token exception callback""" 36 | # check with invalid callback 37 | with pytest.raises(TypeError): 38 | kiteconnect.set_session_expiry_hook(123) 39 | 40 | callback = Mock() 41 | kiteconnect.set_session_expiry_hook(callback) 42 | kiteconnect.set_access_token("some_invalid_token") 43 | with pytest.raises(ex.TokenException): 44 | kiteconnect.orders() 45 | callback.assert_called_with() 46 | 47 | 48 | def test_positions(kiteconnect): 49 | """Test positions.""" 50 | positions = kiteconnect.positions() 51 | mock_resp = utils.get_json_response("portfolio.positions")["data"] 52 | utils.assert_responses(positions, mock_resp) 53 | 54 | 55 | def test_holdings(kiteconnect): 56 | """Test holdings.""" 57 | holdings = kiteconnect.holdings() 58 | mock_resp = utils.get_json_response("portfolio.holdings")["data"] 59 | utils.assert_responses(holdings, mock_resp) 60 | 61 | 62 | def test_auction_instruments(kiteconnect): 63 | """ Test get_auction_instruments """ 64 | auction_inst = kiteconnect.get_auction_instruments() 65 | mock_resp = utils.get_json_response("portfolio.holdings.auction")["data"] 66 | utils.assert_responses(auction_inst, mock_resp) 67 | 68 | 69 | def test_margins(kiteconnect): 70 | """Test margins.""" 71 | margins = kiteconnect.margins() 72 | mock_resp = utils.get_json_response("user.margins")["data"] 73 | utils.assert_responses(margins, mock_resp) 74 | 75 | 76 | def test_margins_segmentwise(kiteconnect): 77 | """Test margins for individual segments.""" 78 | commodity = kiteconnect.margins(segment=kiteconnect.MARGIN_COMMODITY) 79 | assert type(commodity) == dict 80 | 81 | 82 | def test_orders(kiteconnect): 83 | """Test orders get.""" 84 | orders = kiteconnect.orders() 85 | assert type(orders) == list 86 | 87 | 88 | def test_order_history(kiteconnect): 89 | """Test individual order get.""" 90 | orders = kiteconnect.orders() 91 | 92 | if len(orders) == 0: 93 | warnings.warn(UserWarning("Order info: Couldn't perform individual order test since orderbook is empty.")) 94 | return 95 | 96 | order = kiteconnect.order_history(order_id=orders[0]["order_id"]) 97 | 98 | mock_resp = utils.get_json_response("order.info")["data"] 99 | utils.assert_responses(order, mock_resp) 100 | 101 | # check order info statuses order. if its not REJECTED order 102 | # for o in order: 103 | # if "REJECTED" not in o["status"]: 104 | # assert "RECEIVED" in o["status"].upper() 105 | # break 106 | 107 | # assert order[-1]["status"] in ["OPEN", "COMPLETE", "REJECTED"] 108 | 109 | 110 | def test_trades(kiteconnect): 111 | """Test trades.""" 112 | trades = kiteconnect.trades() 113 | mock_resp = utils.get_json_response("trades")["data"] 114 | utils.assert_responses(trades, mock_resp) 115 | 116 | 117 | def test_order_trades(kiteconnect): 118 | """Test individual order get.""" 119 | trades = kiteconnect.trades() 120 | 121 | if len(trades) == 0: 122 | warnings.warn(UserWarning("Trades: Couldn't perform individual order test since trades is empty.")) 123 | return 124 | 125 | order_trades = kiteconnect.order_trades(order_id=trades[0]["order_id"]) 126 | 127 | mock_resp = utils.get_json_response("order.trades")["data"] 128 | utils.assert_responses(order_trades, mock_resp) 129 | 130 | 131 | def test_all_instruments(kiteconnect): 132 | """Test mf instruments fetch.""" 133 | instruments = kiteconnect.instruments() 134 | mock_resp = kiteconnect._parse_instruments(utils.get_response("market.instruments")) 135 | utils.assert_responses(instruments, mock_resp) 136 | 137 | 138 | def test_exchange_instruments(kiteconnect): 139 | """Test mf instruments fetch.""" 140 | instruments = kiteconnect.instruments(exchange=kiteconnect.EXCHANGE_NSE) 141 | mock_resp = kiteconnect._parse_instruments(utils.get_response("market.instruments")) 142 | utils.assert_responses(instruments, mock_resp) 143 | 144 | 145 | def test_mf_orders(kiteconnect): 146 | """Test mf orders get.""" 147 | mf_orders = kiteconnect.mf_orders() 148 | mock_resp = utils.get_json_response("mf.orders")["data"] 149 | utils.assert_responses(mf_orders, mock_resp) 150 | 151 | 152 | def test_mf_order_info(kiteconnect): 153 | """Test mf orders get.""" 154 | orders = kiteconnect.mf_orders() 155 | 156 | if len(orders) == 0: 157 | warnings.warn(UserWarning("MF order info: Couldn't perform individual order test since orderbook is empty.")) 158 | return 159 | 160 | order = kiteconnect.mf_orders(order_id=orders[0]["order_id"]) 161 | 162 | mock_resp = utils.get_json_response("mf.order.info")["data"] 163 | utils.assert_responses(order, mock_resp) 164 | 165 | 166 | def test_mf_sips(kiteconnect): 167 | """Test mf sips get.""" 168 | mf_sips = kiteconnect.mf_sips() 169 | mock_resp = utils.get_json_response("mf.sips")["data"] 170 | utils.assert_responses(mf_sips, mock_resp) 171 | 172 | 173 | def test_mf_holdings(kiteconnect): 174 | """Test mf holdings.""" 175 | mf_holdings = kiteconnect.mf_holdings() 176 | mock_resp = utils.get_json_response("mf.holdings")["data"] 177 | utils.assert_responses(mf_holdings, mock_resp) 178 | 179 | 180 | def test_mf_instruments(kiteconnect): 181 | """Test mf instruments fetch.""" 182 | mf_instruments = kiteconnect.mf_instruments() 183 | mock_resp = kiteconnect._parse_mf_instruments(utils.get_response("mf.instruments")) 184 | utils.assert_responses(mf_instruments, mock_resp) 185 | 186 | 187 | # Historical API tests 188 | ###################### 189 | 190 | @pytest.mark.parametrize("max_interval,candle_interval", [ 191 | (30, "minute"), 192 | (365, "hour"), 193 | (2000, "day"), 194 | (90, "3minute"), 195 | (90, "5minute"), 196 | (90, "10minute"), 197 | (180, "15minute"), 198 | (180, "30minute"), 199 | (365, "60minute") 200 | ], ids=[ 201 | "minute", 202 | "hour", 203 | "day", 204 | "3minute", 205 | "5minute", 206 | "10minute", 207 | "15minute", 208 | "30minute", 209 | "60minute", 210 | ]) 211 | def test_historical_data_intervals(max_interval, candle_interval, kiteconnect): 212 | """Test historical data for each intervals""" 213 | # Reliance token 214 | instrument_token = 256265 215 | to_date = datetime.datetime.now() 216 | diff = int(max_interval / 3) 217 | 218 | from_date = (to_date - datetime.timedelta(days=diff)) 219 | 220 | # minute data 221 | data = kiteconnect.historical_data(instrument_token, from_date, to_date, candle_interval) 222 | mock_resp = kiteconnect._format_historical(utils.get_json_response("market.historical")["data"]) 223 | utils.assert_responses(data, mock_resp) 224 | 225 | # Max interval 226 | from_date = (to_date - datetime.timedelta(days=(max_interval + 1))) 227 | with pytest.raises(ex.InputException): 228 | kiteconnect.historical_data(instrument_token, from_date, to_date, candle_interval) 229 | 230 | 231 | def test_quote(kiteconnect): 232 | """Test quote.""" 233 | instruments = ["NSE:INFY"] 234 | 235 | # Test sending instruments as a list 236 | time.sleep(1.1) 237 | quote = kiteconnect.quote(instruments) 238 | mock_resp = utils.get_json_response("market.quote")["data"] 239 | utils.assert_responses(quote, mock_resp) 240 | 241 | # Test sending instruments as args 242 | time.sleep(1.1) 243 | quote = kiteconnect.quote(*instruments) 244 | mock_resp = utils.get_json_response("market.quote")["data"] 245 | utils.assert_responses(quote, mock_resp) 246 | 247 | 248 | def test_quote_ohlc(kiteconnect): 249 | """Test ohlc.""" 250 | instruments = ["NSE:INFY"] 251 | 252 | # Test sending instruments as a list 253 | time.sleep(1.1) 254 | ohlc = kiteconnect.ohlc(instruments) 255 | mock_resp = utils.get_json_response("market.quote.ohlc")["data"] 256 | utils.assert_responses(ohlc, mock_resp) 257 | 258 | # Test sending instruments as args 259 | time.sleep(1.1) 260 | ohlc = kiteconnect.ohlc(*instruments) 261 | mock_resp = utils.get_json_response("market.quote.ohlc")["data"] 262 | utils.assert_responses(ohlc, mock_resp) 263 | 264 | 265 | def test_quote_ltp(kiteconnect): 266 | """Test ltp.""" 267 | instruments = ["NSE:INFY"] 268 | 269 | # Test sending instruments as a list 270 | time.sleep(1.1) 271 | ltp = kiteconnect.ltp(instruments) 272 | mock_resp = utils.get_json_response("market.quote.ltp")["data"] 273 | utils.assert_responses(ltp, mock_resp) 274 | 275 | # Test sending instruments as args 276 | time.sleep(1.1) 277 | ltp = kiteconnect.ltp(*instruments) 278 | mock_resp = utils.get_json_response("market.quote.ltp")["data"] 279 | utils.assert_responses(ltp, mock_resp) 280 | 281 | 282 | def test_trigger_range(kiteconnect): 283 | """Test ltp.""" 284 | instruments = ["NSE:INFY", "NSE:RELIANCE"] 285 | 286 | # Test sending instruments as a list 287 | buy_resp = kiteconnect.trigger_range(kiteconnect.TRANSACTION_TYPE_BUY, *instruments) 288 | mock_resp = utils.get_json_response("market.trigger_range")["data"] 289 | utils.assert_responses(buy_resp, mock_resp) 290 | 291 | buy_resp = kiteconnect.trigger_range(kiteconnect.TRANSACTION_TYPE_SELL, *instruments) 292 | mock_resp = utils.get_json_response("market.trigger_range")["data"] 293 | utils.assert_responses(buy_resp, mock_resp) 294 | 295 | # Test sending instruments as a args 296 | buy_resp = kiteconnect.trigger_range(kiteconnect.TRANSACTION_TYPE_BUY, instruments) 297 | mock_resp = utils.get_json_response("market.trigger_range")["data"] 298 | utils.assert_responses(buy_resp, mock_resp) 299 | 300 | buy_resp = kiteconnect.trigger_range(kiteconnect.TRANSACTION_TYPE_SELL, instruments) 301 | mock_resp = utils.get_json_response("market.trigger_range")["data"] 302 | utils.assert_responses(buy_resp, mock_resp) 303 | -------------------------------------------------------------------------------- /tests/integration/test_connect_write.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | # import pytest 4 | import utils 5 | import time 6 | import warnings 7 | # import kiteconnect.exceptions as ex 8 | 9 | params = { 10 | "exchange": "NSE", 11 | "tradingsymbol": "RELIANCE", 12 | "transaction_type": "BUY", 13 | "quantity": 1 14 | } 15 | 16 | 17 | def is_pending_order(status): 18 | """Check if the status is pending order status.""" 19 | status = status.upper() 20 | if ("COMPLETE" in status or "REJECT" in status or "CANCEL" in status): 21 | return False 22 | return True 23 | 24 | 25 | def setup_order_place(kiteconnect, 26 | variety, 27 | product, 28 | order_type, 29 | diff_constant=0.01, 30 | price_diff=1, 31 | price=None, 32 | validity=None, 33 | disclosed_quantity=None, 34 | trigger_price=None, 35 | tag="itest"): 36 | """Place an order with custom fields enabled. Prices are calculated from live ltp and offset based 37 | on `price_diff` and `diff_constant.`""" 38 | updated_params = utils.merge_dicts(params, { 39 | "product": product, 40 | "variety": variety, 41 | "order_type": order_type 42 | }) 43 | 44 | # NOT WORKING CURRENTLY 45 | # Raises exception since no price set 46 | # with pytest.raises(ex.InputException): 47 | # kiteconnect.place_order(**updated_params) 48 | 49 | if price or trigger_price: 50 | symbol = params["exchange"] + ":" + params["tradingsymbol"] 51 | ltp = kiteconnect.ltp(symbol) 52 | 53 | # Subtract last price with diff_constant % 54 | diff = ltp[symbol]["last_price"] * diff_constant 55 | round_off_decimal = diff % price_diff if price_diff > 0 else 0 56 | base_price = ltp[symbol]["last_price"] - (diff - round_off_decimal) 57 | 58 | if price and trigger_price: 59 | updated_params["price"] = base_price 60 | updated_params["trigger_price"] = base_price - price_diff 61 | elif price: 62 | updated_params["price"] = base_price 63 | elif trigger_price: 64 | updated_params["trigger_price"] = base_price 65 | 66 | order_id = kiteconnect.place_order(**updated_params) 67 | 68 | # delay order fetch so order is not in received state 69 | time.sleep(0.5) 70 | order = kiteconnect.order_history(order_id) 71 | 72 | return (updated_params, order_id, order) 73 | 74 | 75 | def cleanup_orders(kiteconnect, order_id=None): 76 | """Cleanup all pending orders and exit position for test symbol.""" 77 | order = kiteconnect.order_history(order_id) 78 | status = order[-1]["status"].upper() 79 | variety = order[-1]["variety"] 80 | exchange = order[-1]["exchange"] 81 | product = order[-1]["product"] 82 | tradingsymbol = order[-1]["tradingsymbol"] 83 | parent_order_id = order[-1]["parent_order_id"] 84 | 85 | # Cancel order if order is open 86 | if is_pending_order(status): 87 | kiteconnect.cancel_order(variety=variety, order_id=order_id, parent_order_id=parent_order_id) 88 | # If complete then fetch positions and exit 89 | elif "COMPLETE" in status: 90 | positions = kiteconnect.positions() 91 | for p in positions["net"]: 92 | if (p["tradingsymbol"] == tradingsymbol and 93 | p["exchange"] == exchange and 94 | p["product"] == product and 95 | p["quantity"] != 0 and 96 | p["product"] not in [kiteconnect.PRODUCT_BO, kiteconnect.PRODUCT_CO]): 97 | 98 | updated_params = { 99 | "tradingsymbol": p["tradingsymbol"], 100 | "exchange": p["exchange"], 101 | "transaction_type": "BUY" if p["quantity"] < 0 else "SELL", 102 | "quantity": abs(p["quantity"]), 103 | "product": p["product"], 104 | "variety": kiteconnect.VARIETY_REGULAR, 105 | "order_type": kiteconnect.ORDER_TYPE_MARKET 106 | } 107 | 108 | kiteconnect.place_order(**updated_params) 109 | 110 | # If order is complete and CO/BO order then exit the orde 111 | if "COMPLETE" in status and variety in [kiteconnect.VARIETY_BO, kiteconnect.VARIETY_CO]: 112 | orders = kiteconnect.orders() 113 | leg_order_id = None 114 | for o in orders: 115 | if o["parent_order_id"] == order_id: 116 | leg_order_id = o["order_id"] 117 | break 118 | 119 | if leg_order_id: 120 | kiteconnect.exit_order(variety=variety, order_id=leg_order_id, parent_order_id=order_id) 121 | 122 | 123 | # Order place tests 124 | ##################### 125 | 126 | def test_place_order_market_regular(kiteconnect): 127 | """Place regular marker order.""" 128 | updated_params, order_id, order = setup_order_place( 129 | kiteconnect=kiteconnect, 130 | product=kiteconnect.PRODUCT_MIS, 131 | variety=kiteconnect.VARIETY_REGULAR, 132 | order_type=kiteconnect.ORDER_TYPE_MARKET 133 | ) 134 | 135 | assert order[-1]["product"] == kiteconnect.PRODUCT_MIS 136 | assert order[-1]["variety"] == kiteconnect.VARIETY_REGULAR 137 | 138 | try: 139 | cleanup_orders(kiteconnect, order_id) 140 | except Exception as e: 141 | warnings.warn(UserWarning("Error while cleaning up orders: {}".format(e))) 142 | 143 | 144 | def test_place_order_limit_regular(kiteconnect): 145 | """Place regular limit order.""" 146 | updated_params, order_id, order = setup_order_place( 147 | kiteconnect=kiteconnect, 148 | product=kiteconnect.PRODUCT_MIS, 149 | variety=kiteconnect.VARIETY_REGULAR, 150 | order_type=kiteconnect.ORDER_TYPE_LIMIT, 151 | price=True 152 | ) 153 | 154 | assert order[-1]["product"] == kiteconnect.PRODUCT_MIS 155 | assert order[-1]["variety"] == kiteconnect.VARIETY_REGULAR 156 | 157 | try: 158 | cleanup_orders(kiteconnect, order_id) 159 | except Exception as e: 160 | warnings.warn(UserWarning("Error while cleaning up orders: {}".format(e))) 161 | 162 | 163 | def test_place_order_sl_regular(kiteconnect): 164 | """Place regular SL order.""" 165 | updated_params, order_id, order = setup_order_place( 166 | kiteconnect=kiteconnect, 167 | product=kiteconnect.PRODUCT_MIS, 168 | variety=kiteconnect.VARIETY_REGULAR, 169 | order_type=kiteconnect.ORDER_TYPE_SL, 170 | price=True, 171 | trigger_price=True 172 | ) 173 | 174 | assert order[-1]["product"] == kiteconnect.PRODUCT_MIS 175 | assert order[-1]["variety"] == kiteconnect.VARIETY_REGULAR 176 | assert order[-1]["trigger_price"] 177 | assert order[-1]["price"] 178 | 179 | try: 180 | cleanup_orders(kiteconnect, order_id) 181 | except Exception as e: 182 | warnings.warn(UserWarning("Error while cleaning up orders: {}".format(e))) 183 | 184 | 185 | def test_place_order_slm_regular(kiteconnect): 186 | """Place regular SL-M order.""" 187 | updated_params, order_id, order = setup_order_place( 188 | kiteconnect=kiteconnect, 189 | product=kiteconnect.PRODUCT_MIS, 190 | variety=kiteconnect.VARIETY_REGULAR, 191 | order_type=kiteconnect.ORDER_TYPE_SLM, 192 | trigger_price=True 193 | ) 194 | 195 | assert order[-1]["trigger_price"] 196 | assert order[-1]["price"] == 0 197 | assert order[-1]["product"] == kiteconnect.PRODUCT_MIS 198 | assert order[-1]["variety"] == kiteconnect.VARIETY_REGULAR 199 | 200 | try: 201 | cleanup_orders(kiteconnect, order_id) 202 | except Exception as e: 203 | warnings.warn(UserWarning("Error while cleaning up orders: {}".format(e))) 204 | 205 | 206 | def test_place_order_tag(kiteconnect): 207 | """Send custom tag and get it in orders.""" 208 | tag = "mytag" 209 | updated_params = utils.merge_dicts(params, { 210 | "product": kiteconnect.PRODUCT_MIS, 211 | "variety": kiteconnect.VARIETY_REGULAR, 212 | "order_type": kiteconnect.ORDER_TYPE_MARKET, 213 | "tag": tag 214 | }) 215 | 216 | order_id = kiteconnect.place_order(**updated_params) 217 | order_info = kiteconnect.order_history(order_id=order_id) 218 | assert order_info[0]["tag"] == tag 219 | 220 | try: 221 | cleanup_orders(kiteconnect, order_id) 222 | except Exception as e: 223 | warnings.warn(UserWarning("Error while cleaning up orders: {}".format(e))) 224 | 225 | 226 | def test_place_order_co_market(kiteconnect): 227 | """Place market CO order.""" 228 | updated_params, order_id, order = setup_order_place( 229 | kiteconnect=kiteconnect, 230 | product=kiteconnect.PRODUCT_MIS, 231 | variety=kiteconnect.VARIETY_CO, 232 | order_type=kiteconnect.ORDER_TYPE_MARKET, 233 | trigger_price=True 234 | ) 235 | 236 | assert order[-1]["product"] == kiteconnect.PRODUCT_CO 237 | assert order[-1]["variety"] == kiteconnect.VARIETY_CO 238 | 239 | try: 240 | cleanup_orders(kiteconnect, order_id) 241 | except Exception as e: 242 | warnings.warn(UserWarning("Error while cleaning up orders: {}".format(e))) 243 | 244 | 245 | def test_place_order_co_limit(kiteconnect): 246 | """Place LIMIT co order.""" 247 | updated_params, order_id, order = setup_order_place( 248 | kiteconnect=kiteconnect, 249 | product=kiteconnect.PRODUCT_MIS, 250 | variety=kiteconnect.VARIETY_CO, 251 | order_type=kiteconnect.ORDER_TYPE_LIMIT, 252 | trigger_price=True 253 | ) 254 | 255 | assert order[-1]["product"] == kiteconnect.PRODUCT_CO 256 | assert order[-1]["variety"] == kiteconnect.VARIETY_CO 257 | 258 | try: 259 | cleanup_orders(kiteconnect, order_id) 260 | except Exception as e: 261 | warnings.warn(UserWarning("Error while cleaning up orders: {}".format(e))) 262 | 263 | # Regular order modify and cancel 264 | ################################ 265 | 266 | 267 | def setup_order_modify_cancel(kiteconnect, variety): 268 | symbol = params["exchange"] + ":" + params["tradingsymbol"] 269 | ltp = kiteconnect.ltp(symbol) 270 | 271 | updated_params = utils.merge_dicts(params, { 272 | "product": kiteconnect.PRODUCT_MIS, 273 | "variety": variety, 274 | "order_type": kiteconnect.ORDER_TYPE_LIMIT 275 | }) 276 | 277 | diff = ltp[symbol]["last_price"] * 0.01 278 | updated_params["price"] = ltp[symbol]["last_price"] - (diff - (diff % 1)) 279 | order_id = kiteconnect.place_order(**updated_params) 280 | 281 | # delay order fetch so order is not in received state 282 | time.sleep(0.5) 283 | 284 | order = kiteconnect.order_history(order_id) 285 | status = order[-1]["status"].upper() 286 | if not is_pending_order(status): 287 | warnings.warn(UserWarning("Order is not open with status: ", status)) 288 | return 289 | 290 | return (updated_params, order_id, order) 291 | 292 | 293 | def test_order_cancel_regular(kiteconnect): 294 | """Regular order cancel.""" 295 | setup = setup_order_modify_cancel(kiteconnect, kiteconnect.VARIETY_REGULAR) 296 | if setup: 297 | updated_params, order_id, order = setup 298 | else: 299 | return 300 | 301 | returned_order_id = kiteconnect.cancel_order(updated_params["variety"], order_id) 302 | assert returned_order_id == order_id 303 | time.sleep(0.5) 304 | 305 | order = kiteconnect.order_history(order_id) 306 | status = order[-1]["status"].upper() 307 | assert "CANCELLED" in status 308 | 309 | try: 310 | cleanup_orders(kiteconnect, order_id) 311 | except Exception as e: 312 | warnings.warn(UserWarning("Error while cleaning up orders: {}".format(e))) 313 | 314 | 315 | def test_order_modify_limit_regular(kiteconnect): 316 | """Modify limit regular.""" 317 | setup = setup_order_modify_cancel(kiteconnect, kiteconnect.VARIETY_REGULAR) 318 | if setup: 319 | updated_params, order_id, order = setup 320 | else: 321 | return 322 | 323 | assert order[-1]["quantity"] == updated_params["quantity"] 324 | assert order[-1]["price"] == updated_params["price"] 325 | 326 | to_quantity = 2 327 | to_price = updated_params["price"] - 1 328 | kiteconnect.modify_order(updated_params["variety"], order_id, quantity=to_quantity, price=to_price) 329 | time.sleep(0.5) 330 | 331 | order = kiteconnect.order_history(order_id) 332 | assert order[-1]["quantity"] == to_quantity 333 | assert order[-1]["price"] == to_price 334 | 335 | try: 336 | cleanup_orders(kiteconnect, order_id) 337 | except Exception as e: 338 | warnings.warn(UserWarning("Error while cleaning up orders: {}".format(e))) 339 | 340 | 341 | def test_order_cancel_amo(kiteconnect): 342 | setup = setup_order_modify_cancel(kiteconnect, kiteconnect.VARIETY_AMO) 343 | if setup: 344 | updated_params, order_id, order = setup 345 | else: 346 | return 347 | 348 | returned_order_id = kiteconnect.cancel_order(updated_params["variety"], order_id) 349 | assert returned_order_id == order_id 350 | time.sleep(0.5) 351 | 352 | order = kiteconnect.order_history(order_id) 353 | status = order[-1]["status"].upper() 354 | assert "CANCELLED" in status 355 | 356 | try: 357 | cleanup_orders(kiteconnect, order_id) 358 | except Exception as e: 359 | warnings.warn(UserWarning("Error while cleaning up orders: {}".format(e))) 360 | 361 | 362 | def test_order_modify_limit_amo(kiteconnect): 363 | setup = setup_order_modify_cancel(kiteconnect, kiteconnect.VARIETY_AMO) 364 | if setup: 365 | updated_params, order_id, order = setup 366 | else: 367 | return 368 | 369 | assert order[-1]["quantity"] == updated_params["quantity"] 370 | assert order[-1]["price"] == updated_params["price"] 371 | 372 | to_quantity = 2 373 | to_price = updated_params["price"] - 1 374 | kiteconnect.modify_order(updated_params["variety"], order_id, quantity=to_quantity, price=to_price) 375 | time.sleep(0.5) 376 | 377 | order = kiteconnect.order_history(order_id) 378 | assert order[-1]["quantity"] == to_quantity 379 | assert order[-1]["price"] == to_price 380 | 381 | try: 382 | cleanup_orders(kiteconnect, order_id) 383 | except Exception as e: 384 | warnings.warn(UserWarning("Error while cleaning up orders: {}".format(e))) 385 | 386 | # CO order modify/cancel and exit 387 | ################################# 388 | 389 | 390 | def test_exit_order_co_market_leg(kiteconnect): 391 | updated_params, order_id, order = setup_order_place( 392 | kiteconnect=kiteconnect, 393 | product=kiteconnect.PRODUCT_MIS, 394 | variety=kiteconnect.VARIETY_CO, 395 | order_type=kiteconnect.ORDER_TYPE_MARKET, 396 | trigger_price=True 397 | ) 398 | 399 | assert order[-1]["product"] == kiteconnect.PRODUCT_CO 400 | assert order[-1]["variety"] == kiteconnect.VARIETY_CO 401 | 402 | status = order[-1]["status"] 403 | if "COMPLETE" not in status: 404 | warnings.warn(UserWarning("Order is not complete with status: ", status)) 405 | return 406 | 407 | orders = kiteconnect.orders() 408 | 409 | leg_order = None 410 | for o in orders: 411 | if o["parent_order_id"] == order_id: 412 | leg_order = o 413 | exit 414 | 415 | kiteconnect.exit_order(variety=kiteconnect.VARIETY_CO, order_id=leg_order["order_id"], parent_order_id=order_id) 416 | time.sleep(0.5) 417 | leg_order_info = kiteconnect.order_history(order_id=leg_order["order_id"]) 418 | assert not is_pending_order(leg_order_info[-1]["status"]) 419 | 420 | 421 | def test_cancel_order_co_limit(kiteconnect): 422 | updated_params, order_id, order = setup_order_place( 423 | kiteconnect=kiteconnect, 424 | product=kiteconnect.PRODUCT_MIS, 425 | variety=kiteconnect.VARIETY_CO, 426 | order_type=kiteconnect.ORDER_TYPE_LIMIT, 427 | trigger_price=True, 428 | price=True 429 | ) 430 | 431 | status = order[-1]["status"] 432 | if not is_pending_order(status): 433 | warnings.warn(UserWarning("Order is not pending with status: ", status)) 434 | return 435 | 436 | assert order[-1]["product"] == kiteconnect.PRODUCT_CO 437 | assert order[-1]["variety"] == kiteconnect.VARIETY_CO 438 | 439 | kiteconnect.cancel_order(variety=kiteconnect.VARIETY_CO, order_id=order_id) 440 | time.sleep(0.5) 441 | updated_order = kiteconnect.order_history(order_id=order_id) 442 | assert not is_pending_order(updated_order[-1]["status"]) 443 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zerodha/pykiteconnect/6b7b7621e575411921b506203b526bf275a702c7/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/conftest.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """Pytest config.""" 4 | import os 5 | import sys 6 | import pytest 7 | from kiteconnect import KiteConnect, KiteTicker 8 | 9 | sys.path.append(os.path.join(os.path.dirname(__file__), '../helpers')) 10 | 11 | 12 | @pytest.fixture() 13 | def kiteconnect(): 14 | """Init Kite connect object.""" 15 | kiteconnect = KiteConnect(api_key='', access_token='') 16 | kiteconnect.root = 'http://kite_trade_test' 17 | return kiteconnect 18 | 19 | 20 | @pytest.fixture() 21 | def kiteconnect_with_pooling(): 22 | """Init kite connect object with pooling.""" 23 | kiteconnect = KiteConnect( 24 | api_key="", 25 | access_token="", 26 | pool={ 27 | "pool_connections": 20, 28 | "pool_maxsize": 10, 29 | "max_retries": 2, 30 | "pool_block": False 31 | } 32 | ) 33 | return kiteconnect 34 | 35 | 36 | @pytest.fixture() 37 | def kiteticker(): 38 | """Init Kite ticker object.""" 39 | kws = KiteTicker("", "", "", debug=True, reconnect=False) 40 | kws.socket_url = "ws://127.0.0.1:9000?api_key=?&user_id=&public_token=" 41 | return kws 42 | 43 | 44 | @pytest.fixture() 45 | def protocol(): 46 | from autobahn.test import FakeTransport 47 | from kiteconnect.ticker import KiteTickerClientProtocol,\ 48 | KiteTickerClientFactory 49 | 50 | t = FakeTransport() 51 | f = KiteTickerClientFactory() 52 | p = KiteTickerClientProtocol() 53 | p.factory = f 54 | p.transport = t 55 | 56 | p._connectionMade() 57 | p.state = p.STATE_OPEN 58 | p.websocket_version = 18 59 | return p 60 | -------------------------------------------------------------------------------- /tests/unit/test_connect.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import pytest 3 | import responses 4 | import kiteconnect.exceptions as ex 5 | 6 | import utils 7 | 8 | 9 | def test_set_access_token(kiteconnect): 10 | """Check for token exception when invalid token is set.""" 11 | kiteconnect.root = "https://api.kite.trade" 12 | kiteconnect.set_access_token("invalid_token") 13 | with pytest.raises(ex.TokenException): 14 | kiteconnect.positions() 15 | 16 | 17 | @responses.activate 18 | def test_positions(kiteconnect): 19 | """Test positions.""" 20 | responses.add( 21 | responses.GET, 22 | "{0}{1}".format(kiteconnect.root, kiteconnect._routes["portfolio.positions"]), 23 | body=utils.get_response("portfolio.positions"), 24 | content_type="application/json" 25 | ) 26 | positions = kiteconnect.positions() 27 | assert type(positions) == dict 28 | assert "day" in positions 29 | assert "net" in positions 30 | 31 | 32 | @responses.activate 33 | def test_holdings(kiteconnect): 34 | """Test holdings.""" 35 | responses.add( 36 | responses.GET, 37 | "{0}{1}".format(kiteconnect.root, kiteconnect._routes["portfolio.holdings"]), 38 | body=utils.get_response("portfolio.holdings"), 39 | content_type="application/json" 40 | ) 41 | holdings = kiteconnect.holdings() 42 | assert type(holdings) == list 43 | 44 | 45 | @responses.activate 46 | def test_auction_instruments(kiteconnect): 47 | """Test get_auction_instruments.""" 48 | responses.add( 49 | responses.GET, 50 | "{0}{1}".format(kiteconnect.root, kiteconnect._routes["portfolio.holdings.auction"]), 51 | body=utils.get_response("portfolio.holdings.auction"), 52 | content_type="application/json" 53 | ) 54 | auction_inst = kiteconnect.get_auction_instruments() 55 | assert type(auction_inst) == list 56 | 57 | 58 | @responses.activate 59 | def test_margins(kiteconnect): 60 | """Test margins.""" 61 | responses.add( 62 | responses.GET, 63 | "{0}{1}".format(kiteconnect.root, kiteconnect._routes["user.margins"]), 64 | body=utils.get_response("user.margins"), 65 | content_type="application/json" 66 | ) 67 | margins = kiteconnect.margins() 68 | assert type(margins) == dict 69 | assert kiteconnect.MARGIN_EQUITY in margins 70 | assert kiteconnect.MARGIN_COMMODITY in margins 71 | 72 | 73 | @responses.activate 74 | def test_profile(kiteconnect): 75 | """Test profile.""" 76 | responses.add( 77 | responses.GET, 78 | "{0}{1}".format(kiteconnect.root, kiteconnect._routes["user.profile"]), 79 | body=utils.get_response("user.profile"), 80 | content_type="application/json" 81 | ) 82 | profile = kiteconnect.profile() 83 | assert type(profile) == dict 84 | 85 | 86 | @responses.activate 87 | def test_margins_segmentwise(kiteconnect): 88 | """Test margins for individual segments.""" 89 | responses.add( 90 | responses.GET, 91 | "{0}{1}".format( 92 | kiteconnect.root, 93 | kiteconnect._routes["user.margins.segment"].format( 94 | segment=kiteconnect.MARGIN_COMMODITY 95 | ) 96 | ), 97 | body=utils.get_response("user.margins.segment"), 98 | content_type="application/json" 99 | ) 100 | commodity = kiteconnect.margins(segment=kiteconnect.MARGIN_COMMODITY) 101 | assert type(commodity) == dict 102 | 103 | 104 | @responses.activate 105 | def test_orders(kiteconnect): 106 | """Test orders.""" 107 | responses.add( 108 | responses.GET, 109 | "{0}{1}".format(kiteconnect.root, kiteconnect._routes["orders"]), 110 | body=utils.get_response("orders"), 111 | content_type="application/json" 112 | ) 113 | orders = kiteconnect.orders() 114 | assert type(orders) == list 115 | 116 | 117 | @responses.activate 118 | def test_order_history(kiteconnect): 119 | """Test mf orders get.""" 120 | url = kiteconnect._routes["order.info"].format(order_id="abc123") 121 | responses.add( 122 | responses.GET, 123 | "{0}{1}".format(kiteconnect.root, url), 124 | body=utils.get_response("order.info"), 125 | content_type="application/json" 126 | ) 127 | trades = kiteconnect.order_history(order_id="abc123") 128 | assert type(trades) == list 129 | 130 | 131 | @responses.activate 132 | def test_trades(kiteconnect): 133 | """Test trades.""" 134 | responses.add( 135 | responses.GET, 136 | "{0}{1}".format(kiteconnect.root, kiteconnect._routes["trades"]), 137 | body=utils.get_response("trades"), 138 | content_type="application/json" 139 | ) 140 | trades = kiteconnect.trades() 141 | assert type(trades) == list 142 | 143 | 144 | @responses.activate 145 | def test_order_trades(kiteconnect): 146 | """Test order trades.""" 147 | url = kiteconnect._routes["order.trades"].format(order_id="abc123") 148 | responses.add( 149 | responses.GET, 150 | "{0}{1}".format(kiteconnect.root, url), 151 | body=utils.get_response("trades"), 152 | content_type="application/json" 153 | ) 154 | trades = kiteconnect.order_trades(order_id="abc123") 155 | assert type(trades) == list 156 | 157 | 158 | @responses.activate 159 | def test_instruments(kiteconnect): 160 | """Test mf instruments fetch.""" 161 | responses.add( 162 | responses.GET, 163 | "{0}{1}".format(kiteconnect.root, kiteconnect._routes["market.instruments.all"]), 164 | body=utils.get_response("market.instruments.all"), 165 | content_type="text/csv" 166 | ) 167 | trades = kiteconnect.instruments() 168 | assert type(trades) == list 169 | 170 | 171 | @responses.activate 172 | def test_instruments_exchangewise(kiteconnect): 173 | """Test mf instruments fetch.""" 174 | responses.add( 175 | responses.GET, 176 | "{0}{1}".format(kiteconnect.root, 177 | kiteconnect._routes["market.instruments"].format(exchange=kiteconnect.EXCHANGE_NSE)), 178 | body=utils.get_response("market.instruments"), 179 | content_type="text/csv" 180 | ) 181 | trades = kiteconnect.instruments(exchange=kiteconnect.EXCHANGE_NSE) 182 | assert type(trades) == list 183 | 184 | 185 | @responses.activate 186 | def test_mf_orders(kiteconnect): 187 | """Test mf orders get.""" 188 | responses.add( 189 | responses.GET, 190 | "{0}{1}".format(kiteconnect.root, kiteconnect._routes["mf.orders"]), 191 | body=utils.get_response("mf.orders"), 192 | content_type="application/json" 193 | ) 194 | trades = kiteconnect.mf_orders() 195 | assert type(trades) == list 196 | 197 | 198 | @responses.activate 199 | def test_mf_individual_order(kiteconnect): 200 | """Test mf orders get.""" 201 | url = kiteconnect._routes["mf.order.info"].format(order_id="abc123") 202 | responses.add( 203 | responses.GET, 204 | "{0}{1}".format(kiteconnect.root, url), 205 | body=utils.get_response("mf.order.info"), 206 | content_type="application/json" 207 | ) 208 | trades = kiteconnect.mf_orders(order_id="abc123") 209 | assert type(trades) == dict 210 | 211 | 212 | @responses.activate 213 | def test_mf_sips(kiteconnect): 214 | """Test mf sips get.""" 215 | responses.add( 216 | responses.GET, 217 | "{0}{1}".format(kiteconnect.root, kiteconnect._routes["mf.sips"]), 218 | body=utils.get_response("mf.sips"), 219 | content_type="application/json" 220 | ) 221 | trades = kiteconnect.mf_sips() 222 | assert type(trades) == list 223 | 224 | 225 | @responses.activate 226 | def test_mf_individual_sip(kiteconnect): 227 | """Test mf sips get.""" 228 | url = kiteconnect._routes["mf.sip.info"].format(sip_id="abc123") 229 | responses.add( 230 | responses.GET, 231 | "{0}{1}".format(kiteconnect.root, url), 232 | body=utils.get_response("mf.sip.info"), 233 | content_type="application/json" 234 | ) 235 | trades = kiteconnect.mf_sips(sip_id="abc123") 236 | assert type(trades) == dict 237 | 238 | 239 | @responses.activate 240 | def test_mf_holdings(kiteconnect): 241 | """Test mf holdings.""" 242 | responses.add( 243 | responses.GET, 244 | "{0}{1}".format(kiteconnect.root, kiteconnect._routes["mf.holdings"]), 245 | body=utils.get_response("mf.holdings"), 246 | content_type="application/json" 247 | ) 248 | trades = kiteconnect.mf_holdings() 249 | assert type(trades) == list 250 | 251 | 252 | @responses.activate 253 | def test_mf_instruments(kiteconnect): 254 | """Test mf instruments fetch.""" 255 | responses.add( 256 | responses.GET, 257 | "{0}{1}".format(kiteconnect.root, kiteconnect._routes["mf.instruments"]), 258 | body=utils.get_response("mf.instruments"), 259 | content_type="text/csv" 260 | ) 261 | trades = kiteconnect.mf_instruments() 262 | assert type(trades) == list 263 | 264 | 265 | @responses.activate 266 | def test_get_gtts(kiteconnect): 267 | """Test all gtts fetch.""" 268 | responses.add( 269 | responses.GET, 270 | "{0}{1}".format(kiteconnect.root, kiteconnect._routes["gtt"]), 271 | body=utils.get_response("gtt"), 272 | content_type="application/json" 273 | ) 274 | gtts = kiteconnect.get_gtts() 275 | assert type(gtts) == list 276 | 277 | 278 | @responses.activate 279 | def test_get_gtt(kiteconnect): 280 | """Test single gtt fetch.""" 281 | responses.add( 282 | responses.GET, 283 | "{0}{1}".format(kiteconnect.root, kiteconnect._routes["gtt.info"].format(trigger_id=123)), 284 | body=utils.get_response("gtt.info"), 285 | content_type="application/json" 286 | ) 287 | gtts = kiteconnect.get_gtt(123) 288 | print(gtts) 289 | assert gtts["id"] == 123 290 | 291 | 292 | @responses.activate 293 | def test_place_gtt(kiteconnect): 294 | """Test place gtt order.""" 295 | responses.add( 296 | responses.POST, 297 | "{0}{1}".format(kiteconnect.root, kiteconnect._routes["gtt.place"]), 298 | body=utils.get_response("gtt.place"), 299 | content_type="application/json" 300 | ) 301 | gtts = kiteconnect.place_gtt( 302 | trigger_type=kiteconnect.GTT_TYPE_SINGLE, 303 | tradingsymbol="INFY", 304 | exchange="NSE", 305 | trigger_values=[1], 306 | last_price=800, 307 | orders=[{ 308 | "transaction_type": kiteconnect.TRANSACTION_TYPE_BUY, 309 | "quantity": 1, 310 | "order_type": kiteconnect.ORDER_TYPE_LIMIT, 311 | "product": kiteconnect.PRODUCT_CNC, 312 | "price": 1, 313 | }] 314 | ) 315 | assert gtts["trigger_id"] == 123 316 | 317 | 318 | @responses.activate 319 | def test_modify_gtt(kiteconnect): 320 | """Test modify gtt order.""" 321 | responses.add( 322 | responses.PUT, 323 | "{0}{1}".format(kiteconnect.root, kiteconnect._routes["gtt.modify"].format(trigger_id=123)), 324 | body=utils.get_response("gtt.modify"), 325 | content_type="application/json" 326 | ) 327 | gtts = kiteconnect.modify_gtt( 328 | trigger_id=123, 329 | trigger_type=kiteconnect.GTT_TYPE_SINGLE, 330 | tradingsymbol="INFY", 331 | exchange="NSE", 332 | trigger_values=[1], 333 | last_price=800, 334 | orders=[{ 335 | "transaction_type": kiteconnect.TRANSACTION_TYPE_BUY, 336 | "quantity": 1, 337 | "order_type": kiteconnect.ORDER_TYPE_LIMIT, 338 | "product": kiteconnect.PRODUCT_CNC, 339 | "price": 1, 340 | }] 341 | ) 342 | assert gtts["trigger_id"] == 123 343 | 344 | 345 | @responses.activate 346 | def test_delete_gtt(kiteconnect): 347 | """Test delete gtt order.""" 348 | responses.add( 349 | responses.DELETE, 350 | "{0}{1}".format(kiteconnect.root, kiteconnect._routes["gtt.delete"].format(trigger_id=123)), 351 | body=utils.get_response("gtt.delete"), 352 | content_type="application/json" 353 | ) 354 | gtts = kiteconnect.delete_gtt(123) 355 | assert gtts["trigger_id"] == 123 356 | 357 | 358 | @responses.activate 359 | def test_order_margins(kiteconnect): 360 | """ Test order margins and charges """ 361 | responses.add( 362 | responses.POST, 363 | "{0}{1}".format(kiteconnect.root, kiteconnect._routes["order.margins"]), 364 | body=utils.get_response("order.margins"), 365 | content_type="application/json" 366 | ) 367 | order_param_single = [{ 368 | "exchange": "NSE", 369 | "tradingsymbol": "INFY", 370 | "transaction_type": "BUY", 371 | "variety": "regular", 372 | "product": "MIS", 373 | "order_type": "MARKET", 374 | "quantity": 2 375 | }] 376 | 377 | margin_detail = kiteconnect.order_margins(order_param_single) 378 | # Order margins 379 | assert margin_detail[0]['type'] == "equity" 380 | assert margin_detail[0]['total'] != 0 381 | # Order charges 382 | assert margin_detail[0]['charges']['transaction_tax'] != 0 383 | assert margin_detail[0]['charges']['gst']['total'] != 0 384 | 385 | 386 | @responses.activate 387 | def test_basket_order_margins(kiteconnect): 388 | """ Test basket order margins and charges """ 389 | responses.add( 390 | responses.POST, 391 | "{0}{1}".format(kiteconnect.root, kiteconnect._routes["order.margins.basket"]), 392 | body=utils.get_response("order.margins.basket"), 393 | content_type="application/json" 394 | ) 395 | order_param_multi = [{ 396 | "exchange": "NFO", 397 | "tradingsymbol": "NIFTY23JANFUT", 398 | "transaction_type": "BUY", 399 | "variety": "regular", 400 | "product": "MIS", 401 | "order_type": "MARKET", 402 | "quantity": 75 403 | }, 404 | { 405 | "exchange": "NFO", 406 | "tradingsymbol": "NIFTY23JANFUT", 407 | "transaction_type": "BUY", 408 | "variety": "regular", 409 | "product": "MIS", 410 | "order_type": "MARKET", 411 | "quantity": 75 412 | }] 413 | 414 | margin_detail = kiteconnect.basket_order_margins(order_param_multi) 415 | # Order margins 416 | assert margin_detail['orders'][0]['exposure'] != 0 417 | assert margin_detail['orders'][0]['type'] == "equity" 418 | # Order charges 419 | assert margin_detail['orders'][0]['total'] != 0 420 | 421 | @responses.activate 422 | def test_virtual_contract_note(kiteconnect): 423 | """ Test virtual contract note charges """ 424 | responses.add( 425 | responses.POST, 426 | "{0}{1}".format(kiteconnect.root, kiteconnect._routes["order.contract_note"]), 427 | body=utils.get_response("order.contract_note"), 428 | content_type="application/json" 429 | ) 430 | 431 | order_book_params = [{ 432 | "order_id": "111111111", 433 | "exchange": "NSE", 434 | "tradingsymbol": "SBIN", 435 | "transaction_type": "BUY", 436 | "variety": "regular", 437 | "product": "CNC", 438 | "order_type": "MARKET", 439 | "quantity": 1, 440 | "average_price": 560 441 | }, 442 | { 443 | "order_id": "2222222222", 444 | "exchange": "MCX", 445 | "tradingsymbol": "GOLDPETAL23JULFUT", 446 | "transaction_type": "SELL", 447 | "variety": "regular", 448 | "product": "NRML", 449 | "order_type": "LIMIT", 450 | "quantity": 1, 451 | "average_price": 5862 452 | }, 453 | { 454 | "order_id": "3333333333", 455 | "exchange": "NFO", 456 | "tradingsymbol": "NIFTY2371317900PE", 457 | "transaction_type": "BUY", 458 | "variety": "regular", 459 | "product": "NRML", 460 | "order_type": "LIMIT", 461 | "quantity": 100, 462 | "average_price": 1.5 463 | }] 464 | 465 | order_book_charges = kiteconnect.get_virtual_contract_note(order_book_params) 466 | # Order charges 467 | assert order_book_charges[0]['charges']['transaction_tax_type'] == "stt" 468 | assert order_book_charges[0]['charges']['total'] != 0 469 | # CTT tax type 470 | assert order_book_charges[1]['charges']['transaction_tax_type'] == "ctt" 471 | assert order_book_charges[1]['charges']['total'] != 0 472 | -------------------------------------------------------------------------------- /tests/unit/test_exceptions.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | import pytest 4 | import responses 5 | import kiteconnect.exceptions as ex 6 | 7 | 8 | @responses.activate 9 | def test_wrong_json_response(kiteconnect): 10 | responses.add( 11 | responses.GET, 12 | "%s%s" % (kiteconnect.root, kiteconnect._routes["portfolio.positions"]), 13 | body="{a:b}", 14 | content_type="application/json" 15 | ) 16 | with pytest.raises(ex.DataException) as exc: 17 | kiteconnect.positions() 18 | assert exc.message == "Couldn't parse the JSON response "\ 19 | "received from the server: {a:b}" 20 | 21 | 22 | @responses.activate 23 | def test_wrong_content_type(kiteconnect): 24 | rdf_data = "zerodha&v=3" 26 | 27 | def test_request_without_pooling(self, kiteconnect): 28 | assert isinstance(kiteconnect.reqsession, requests.Session) is True 29 | assert kiteconnect.reqsession.request is not None 30 | 31 | def test_request_pooling(self, kiteconnect_with_pooling): 32 | assert isinstance(kiteconnect_with_pooling.reqsession, requests.Session) is True 33 | assert kiteconnect_with_pooling.reqsession.request is not None 34 | http_adapter = kiteconnect_with_pooling.reqsession.adapters['https://'] 35 | assert http_adapter._pool_maxsize == 10 36 | assert http_adapter._pool_connections == 20 37 | assert http_adapter._pool_block is False 38 | assert http_adapter.max_retries.total == 2 39 | 40 | @responses.activate 41 | def test_set_session_expiry_hook_meth(self, kiteconnect): 42 | 43 | def mock_hook(): 44 | raise ex.TokenException("token expired it seems! please login again") 45 | 46 | kiteconnect.set_session_expiry_hook(mock_hook) 47 | 48 | # Now lets try raising TokenException 49 | responses.add( 50 | responses.GET, 51 | "{0}{1}".format(kiteconnect.root, kiteconnect._routes["portfolio.positions"]), 52 | body='{"error_type": "TokenException", "message": "Please login again"}', 53 | content_type="application/json", 54 | status=403 55 | ) 56 | with pytest.raises(ex.TokenException) as exc: 57 | kiteconnect.positions() 58 | assert exc.message == "token expired it seems! please login again" 59 | 60 | def test_set_access_token_meth(self, kiteconnect): 61 | assert kiteconnect.access_token == "" 62 | # Modify the access token now 63 | kiteconnect.set_access_token("") 64 | assert kiteconnect.access_token == "" 65 | # Change it back 66 | kiteconnect.set_access_token("") 67 | 68 | @patch.object(KiteConnect, "_post", get_fake_token) 69 | def test_generate_session(self, kiteconnect): 70 | resp = kiteconnect.generate_session( 71 | request_token="", 72 | api_secret="" 73 | ) 74 | assert resp["access_token"] == "TOKEN" 75 | assert kiteconnect.access_token == "TOKEN" 76 | 77 | # Change it back 78 | kiteconnect.set_access_token("") 79 | 80 | @patch.object(KiteConnect, "_delete", get_fake_delete) 81 | def test_invalidate_token(self, kiteconnect): 82 | resp = kiteconnect.invalidate_access_token(access_token="") 83 | assert resp["message"] == "token invalidated" 84 | -------------------------------------------------------------------------------- /tests/unit/test_ticker.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | """Ticker tests""" 3 | import six 4 | import json 5 | from mock import Mock 6 | from base64 import b64encode 7 | from hashlib import sha1 8 | 9 | from autobahn.websocket.protocol import WebSocketProtocol 10 | 11 | 12 | class TestTicker: 13 | 14 | def test_autoping(self, protocol): 15 | protocol.autoPingInterval = 1 16 | protocol.websocket_protocols = [Mock()] 17 | protocol.websocket_extensions = [] 18 | protocol._onOpen = lambda: None 19 | protocol._wskey = '0' * 24 20 | protocol.peer = Mock() 21 | 22 | # usually provided by the Twisted or asyncio specific 23 | # subclass, but we're testing the parent here... 24 | protocol._onConnect = Mock() 25 | protocol._closeConnection = Mock() 26 | 27 | # set up a connection 28 | protocol.startHandshake() 29 | 30 | key = protocol.websocket_key + WebSocketProtocol._WS_MAGIC 31 | protocol.data = ( 32 | b"HTTP/1.1 101 Switching Protocols\x0d\x0a" 33 | b"Upgrade: websocket\x0d\x0a" 34 | b"Connection: upgrade\x0d\x0a" 35 | b"Sec-Websocket-Accept: " + b64encode(sha1(key).digest()) + b"\x0d\x0a\x0d\x0a" 36 | ) 37 | protocol.processHandshake() 38 | 39 | def test_sendclose(self, protocol): 40 | protocol.sendClose() 41 | 42 | assert protocol.transport._written is not None 43 | assert protocol.state == protocol.STATE_CLOSING 44 | 45 | def test_sendMessage(self, protocol): 46 | assert protocol.state == protocol.STATE_OPEN 47 | 48 | protocol.sendMessage(six.b(json.dumps({"message": "blah"}))) 49 | --------------------------------------------------------------------------------