├── .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 | [](https://pypi.python.org/pypi/kiteconnect)
4 | [](https://travis-ci.org/zerodhatech/pykiteconnect)
5 | [](https://ci.appveyor.com/project/rainmattertech/pykiteconnect)
6 | [](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 |
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 |
--------------------------------------------------------------------------------