├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── python-publish.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── MANIFEST.in ├── README.md ├── alphatrade ├── __init__.py ├── alphatrade.py └── exceptions.py ├── dev-requirements.txt ├── requirements.txt ├── setup.cfg ├── setup.py ├── snaps ├── forget_password.png ├── reset_two_fa.png └── set_answers.png ├── zexample_sas_login.py ├── zhistorical_data.py ├── zlogin_example.py ├── zstop.txt └── zstreaming_data.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/python-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflows will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Set up Python 17 | uses: actions/setup-python@v2 18 | with: 19 | python-version: "3.x" 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | python -m pip install -r dev-requirements.txt --user 24 | python -m pip install -r requirements.txt --user 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | python setup.py sdist bdist_wheel 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # examples access_token.txt 132 | .vscode/settings.json 133 | .vscode/launch.json 134 | config.py 135 | access_token.txt 136 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.linting.pylintEnabled": true, 3 | "python.linting.banditEnabled": false, 4 | "python.linting.enabled": true, 5 | "cSpell.words": [ 6 | "TOTP", 7 | "twofa" 8 | ], 9 | "python.linting.flake8Enabled": false 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 algo2win 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Include the license file 2 | include LICENSE 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python APIs for SAS Online Alpha Trade Web Platform 2 | 3 | # MAJOR CHANGES : NEW VERSION 1.0.0 4 | 5 | ## API endpoints are changed to match the new ones, bugs expected 6 | 7 | 1. Removed check for enabled exchanges, you can now download or search symbols from MCX as well if it is not enabled 8 | 2. TOTP SECRET or TOTP both can be given as argument while creating AlphaTrade object (if it is 6 digits it will conside TOTP else TOTP SECRET) 9 | 3. Added new search function to search scrips which will return json for found scrips, you need to process it further 10 | 4. More functions to come. 11 | 5. Check whether streaming websocket is working or not 12 | 6. The `examples` folder is removed and examples are renamed and kept in root directory for ease of development 13 | 14 | # STEPS to work 15 | 16 | 1. Clone the repo locally - `git clone https://github.com/algo2t/alphatrade.git` 17 | 2. Create a virtualenv - `python -m pip install virtualenv` and then `python -m virtualenv venv` and activate the `venv` environment. 18 | 3. Install dev-requirement.txt - `python -m pip install -r dev-requirements.txt` - this is to ensure `setuptools==57.5.0` is installed. There is a bug with `protlib`, target is to get reed of `protlib` in future 19 | 4. Install requirement.txt - `python -m pip install -r requirement.txt` 20 | 5. Create the `config.py` file in root of cloned repo with `login_id`, `password` and `TOTP` SECRET, you can add the `access_token.txt` if you want to use existing `access_token`. 21 | 6. Try the examples `python zlogin_example.py`, `python zexample_sas_login.py`, `python zhistorical_data.py` and `python zstreaming_data.py` 22 | 7. Expecting issues with the streaming data !!! :P 23 | 24 | 25 | # NOTE:: This is Unofficial python module, don't ask SAS support team for help, use it AS-IS 26 | 27 | The Python APIs for communicating with the SAS Online Alpha Trade Web Platform. 28 | 29 | Alpha Trade Python library provides an easy to use python wrapper over the HTTPS APIs. 30 | 31 | The HTTP calls have been converted to methods and JSON responses are wrapped into Python-compatible objects. 32 | 33 | Websocket connections are handled automatically within the library. 34 | 35 | This work is completely based on Python SDK / APIs for [AliceBlueOnline](https://github.com/krishnavelu/alice_blue.git). 36 | Thanks to [krishnavelu](https://github.com/krishnavelu/). 37 | 38 | - **Author: [algo2t](https://github.com/algo2t/)** 39 | - **Github Repository: [alphatrade](https://github.com/algo2t/alphatrade.git)** 40 | 41 | ## Installation 42 | 43 | This module is installed via pip: 44 | 45 | ``` 46 | pip install git+https://github.com/algo2t/alphatrade.git 47 | ``` 48 | 49 | It can also be installed from [pypi](https://pypi.org/project/alphatrade/1.0.0/) 50 | 51 | ``` 52 | pip install alphatrade 53 | ``` 54 | 55 | To force upgrade existing installations: 56 | 57 | ``` 58 | pip uninstall alphatrade 59 | pip --no-cache-dir install --upgrade alphatrade 60 | ``` 61 | 62 | ### Prerequisites 63 | 64 | Python 3.x 65 | 66 | ## Make sure to install `setuptools==57.5.0` for `protlib==1.5.0` to work properly 67 | 68 | Also, you need the following modules: 69 | 70 | - `setuptools==57.5.0` 71 | - `protlib==1.5.0` 72 | - `websocket-client==1.6.1` 73 | - `requests==2.31.0` 74 | - `pandas==2.0.3` 75 | - `pyotp==2.8.0` 76 | 77 | The modules can also be installed using `pip` 78 | 79 | ## Examples - Start Here - Important 80 | 81 | Please clone this repository and check the examples folder to get started. 82 | Check [here](https://algo2t.github.io/alphatrade/#working-with-examples) 83 | 84 | ## Getting started with API 85 | 86 | ### Overview 87 | 88 | There is only one class in the whole library: `AlphaTrade`. When the `AlphaTrade` object is created an access token from the SAS Online alpha trade server is stored in text file `access_token.txt` in the same directory. An access token is valid for 24 hours. See the examples folder with `config.py` file to see how to store your credentials. 89 | With an access token, you can instantiate an AlphaTrade object again. Ideally you only need to create an access_token once every day. 90 | 91 | ### REST Documentation 92 | 93 | The original REST API that this SDK is based on is available online. 94 | [Tradelabs API documentation](http://primusapi.tradelab.in/webapi/) 95 | 96 | ## Using the API 97 | 98 | ### Logging 99 | 100 | The whole library is equipped with python‘s `logging` module for debugging. If more debug information is needed, enable logging using the following code. 101 | 102 | ```python 103 | import logging 104 | logging.basicConfig(level=logging.DEBUG) 105 | ``` 106 | 107 | ### Get an access token 108 | 109 | 1. Import alphatrade 110 | 111 | ```python 112 | from alphatrade import * 113 | ``` 114 | 115 | 2. Create `config.py` file 116 | Always keep credentials in a separate file 117 | ```python 118 | login_id = "XXXXX" 119 | password = "XXXXXXXX" 120 | Totp = 'XXXXXXXXXXXXXXXX' 121 | 122 | try: 123 | access_token = open('access_token.txt', 'r').read().rstrip() 124 | except Exception as e: 125 | print('Exception occurred :: {}'.format(e)) 126 | access_token = None 127 | ``` 128 | 129 | 3. Import the config 130 | ```python 131 | import config 132 | ``` 133 | 134 | ### Create AlphaTrade Object 135 | 136 | 1. Create `AlphaTrade` object with your `login_id`, `password`, `TOTP` / `TOTP_SECRET` and/or `access_token`. 137 | 138 | Use `config` object to get `login_id`, `password`, `TOTP` and `access_token`. 139 | 140 | ```python 141 | from alphatrade import AlphaTrade 142 | import config 143 | import pyotp 144 | Totp = config.Totp 145 | pin = pyotp.TOTP(Totp).now() 146 | totp = f"{int(pin):06d}" if len(pin) <=5 else pin 147 | sas = AlphaTrade(login_id=config.login_id, password=config.password, twofa=totp, access_token=config.access_token) 148 | 149 | ``` 150 | 151 | ## OR 152 | 153 | ```python 154 | ## filename config.py 155 | 156 | login_id = "RR24XX" 157 | password = "SuperSecretPassword!!!" 158 | TOTP_SECRET = 'YOURTOTPSECRETEXTERNALAUTH' 159 | 160 | try: 161 | access_token = open('access_token.txt', 'r').read().rstrip() 162 | except Exception as e: 163 | print(f'Exception occurred :: {e}') 164 | access_token = None 165 | 166 | ``` 167 | 168 | 169 | ```python 170 | from alphatrade import AlphaTrade 171 | import config 172 | import pyotp 173 | sas = AlphaTrade(login_id=config.login_id, password=config.password, twofa=config.TOTP_SECRET, access_token=config.access_token) 174 | 175 | ``` 176 | 177 | 178 | 2. You can run commands here to check your connectivity 179 | 180 | ```python 181 | print(sas.get_balance()) # get balance / margin limits 182 | print(sas.get_profile()) # get profile 183 | print(sas.get_daywise_positions()) # get daywise positions 184 | print(sas.get_netwise_positions()) # get netwise positions 185 | print(sas.get_holding_positions()) # get holding positions 186 | ``` 187 | 188 | ### Get master contracts 189 | 190 | Getting master contracts allow you to search for instruments by symbol name and place orders. 191 | Master contracts are stored as an OrderedDict by token number and by symbol name. Whenever you get a trade update, order update, or quote update, the library will check if master contracts are loaded. If they are, it will attach the instrument object directly to the update. By default all master contracts of all enabled exchanges in your personal profile will be downloaded. i.e. If your profile contains the following as enabled exchanges `['NSE', 'BSE', 'CDS', 'MCX', NFO']` all contract notes of all exchanges will be downloaded by default. If you feel it takes too much time to download all exchange, or if you don‘t need all exchanges to be downloaded, you can specify which exchange to download contract notes while creating the AlphaTrade object. 192 | 193 | ```python 194 | sas = AlphaTrade(login_id=config.login_id, password=config.password, twofa=totp, access_token=config.access_token, master_contracts_to_download=['NSE', 'BSE']) 195 | ``` 196 | 197 | This will reduce a few milliseconds in object creation time of AlphaTrade object. 198 | 199 | ### Get tradable instruments 200 | 201 | Symbols can be retrieved in multiple ways. Once you have the master contract loaded for an exchange, you can get an instrument in many ways. 202 | 203 | Get a single instrument by it‘s name: 204 | 205 | ```python 206 | tatasteel_nse_eq = sas.get_instrument_by_symbol('NSE', 'TATASTEEL') 207 | reliance_nse_eq = sas.get_instrument_by_symbol('NSE', 'RELIANCE') 208 | ongc_bse_eq = sas.get_instrument_by_symbol('BSE', 'ONGC') 209 | india_vix_nse_index = sas.get_instrument_by_symbol('NSE', 'India VIX') 210 | sensex_nse_index = sas.get_instrument_by_symbol('BSE', 'SENSEX') 211 | ``` 212 | 213 | Get a single instrument by it‘s token number (generally useful only for BSE Equities): 214 | 215 | ```python 216 | ongc_bse_eq = sas.get_instrument_by_token('BSE', 500312) 217 | reliance_bse_eq = sas.get_instrument_by_token('BSE', 500325) 218 | acc_nse_eq = sas.get_instrument_by_token('NSE', 22) 219 | ``` 220 | 221 | Get FNO instruments easily by mentioning expiry, strike & call or put. 222 | 223 | ```python 224 | bn_fut = sas.get_instrument_for_fno(symbol = 'BANKNIFTY', expiry_date=datetime.date(2019, 6, 27), is_fut=True, strike=None, is_call = False) 225 | bn_call = sas.get_instrument_for_fno(symbol = 'BANKNIFTY', expiry_date=datetime.date(2019, 6, 27), is_fut=False, strike=30000, is_call = True) 226 | bn_put = sas.get_instrument_for_fno(symbol = 'BANKNIFTY', expiry_date=datetime.date(2019, 6, 27), is_fut=False, strike=30000, is_call = False) 227 | ``` 228 | 229 | ### Search for symbols 230 | 231 | Search for multiple instruments by matching the name. This works case insensitive and returns all instrument which has the name in its symbol. 232 | 233 | ```python 234 | all_sensex_scrips = sas.search_instruments('BSE', 'sEnSeX') 235 | print(all_sensex_scrips) 236 | ``` 237 | 238 | The above code results multiple symbol which has ‘sensex’ in its symbol. 239 | 240 | ``` 241 | [Instrument(exchange='BSE', token=1, symbol='SENSEX', name='SENSEX', expiry=None, lot_size=None), Instrument(exchange='BSE', token=540154, symbol='IDFSENSEXE B', name='IDFC Mutual Fund', expiry=None, lot_size=None), Instrument(exchange='BSE', token=532985, symbol='KTKSENSEX B', name='KOTAK MAHINDRA MUTUAL FUND', expiry=None, lot_size=None), Instrument(exchange='BSE', token=538683, symbol='NETFSENSEX B', name='NIPPON INDIA ETF SENSEX', expiry=None, lot_size=None), Instrument(exchange='BSE', token=535276, symbol='SBISENSEX B', name='SBI MUTUAL FUND - SBI ETF SENS', expiry=None, lot_size=None)] 242 | ``` 243 | 244 | Search for multiple instruments by matching multiple names 245 | 246 | ```python 247 | multiple_underlying = ['BANKNIFTY','NIFTY','INFY','BHEL'] 248 | all_scripts = sas.search_instruments('NFO', multiple_underlying) 249 | ``` 250 | 251 | #### Instrument object 252 | 253 | Instruments are represented by instrument objects. These are named-tuples that are created while getting the master contracts. They are used when placing an order and searching for an instrument. The structure of an instrument tuple is as follows: 254 | 255 | ```python 256 | Instrument = namedtuple('Instrument', ['exchange', 'token', 'symbol', 257 | 'name', 'expiry', 'lot_size']) 258 | ``` 259 | 260 | All instruments have the fields mentioned above. Wherever a field is not applicable for an instrument (for example, equity instruments don‘t have strike prices), that value will be `None` 261 | 262 | ### Quote update 263 | 264 | Once you have master contracts loaded, you can easily subscribe to quote updates. 265 | 266 | #### Four types of feed data are available 267 | 268 | You can subscribe any one type of quote update for a given scrip. Using the `LiveFeedType` enum, you can specify what type of live feed you need. 269 | 270 | - `LiveFeedType.MARKET_DATA` 271 | - `LiveFeedType.COMPACT` 272 | - `LiveFeedType.SNAPQUOTE` 273 | - `LiveFeedType.FULL_SNAPQUOTE` 274 | 275 | Please refer to the original documentation [here](http://primusapi.tradelab.in/webapi/) for more details of different types of quote update. 276 | 277 | #### Subscribe to a live feed 278 | 279 | ```python 280 | sas.subscribe(sas.get_instrument_by_symbol('NSE', 'TATASTEEL'), LiveFeedType.MARKET_DATA) 281 | sas.subscribe(sas.get_instrument_by_symbol('BSE', 'RELIANCE'), LiveFeedType.COMPACT) 282 | ``` 283 | 284 | Subscribe to multiple instruments in a single call. Give an array of instruments to be subscribed. 285 | 286 | ```python 287 | sas.subscribe([sas.get_instrument_by_symbol('NSE', 'TATASTEEL'), sas.get_instrument_by_symbol('NSE', 'ACC')], LiveFeedType.MARKET_DATA) 288 | ``` 289 | 290 | Note: There is a limit of 250 scrips that can be subscribed on total. Beyond this point the server may disconnect web-socket connection. 291 | 292 | Start getting live feed via socket 293 | 294 | ```python 295 | socket_opened = False 296 | def event_handler_quote_update(message): 297 | print(f"quote update {message}") 298 | 299 | def open_callback(): 300 | global socket_opened 301 | socket_opened = True 302 | 303 | sas.start_websocket(subscribe_callback=event_handler_quote_update, 304 | socket_open_callback=open_callback, 305 | run_in_background=True) 306 | while(socket_opened==False): 307 | pass 308 | sas.subscribe(sas.get_instrument_by_symbol('NSE', 'ONGC'), LiveFeedType.MARKET_DATA) 309 | sleep(10) 310 | ``` 311 | 312 | #### Unsubscribe to a live feed 313 | 314 | Unsubscribe to an existing live feed 315 | 316 | ```python 317 | sas.unsubscribe(sas.get_instrument_by_symbol('NSE', 'TATASTEEL'), LiveFeedType.MARKET_DATA) 318 | sas.unsubscribe(sas.get_instrument_by_symbol('BSE', 'RELIANCE'), LiveFeedType.COMPACT) 319 | ``` 320 | 321 | Unsubscribe to multiple instruments in a single call. Give an array of instruments to be unsubscribed. 322 | 323 | ```python 324 | sas.unsubscribe([sas.get_instrument_by_symbol('NSE', 'TATASTEEL'), sas.get_instrument_by_symbol('NSE', 'ACC')], LiveFeedType.MARKET_DATA) 325 | ``` 326 | 327 | #### Get All Subscribed Symbols 328 | 329 | ```python 330 | sas.get_all_subscriptions() # All 331 | ``` 332 | 333 | ### Market Status messages & Exchange messages. 334 | 335 | Subscribe to market status messages 336 | 337 | ```python 338 | sas.subscribe_market_status_messages() 339 | ``` 340 | 341 | Getting market status messages. 342 | 343 | ```python 344 | print(sas.get_market_status_messages()) 345 | ``` 346 | 347 | Example result of `get_market_status_messages()` 348 | 349 | ``` 350 | [{'exchange': 'NSE', 'length_of_market_type': 6, 'market_type': b'NORMAL', 'length_of_status': 31, 'status': b'The Closing Session has closed.'}, {'exchange': 'NFO', 'length_of_market_type': 6, 'market_type': b'NORMAL', 'length_of_status': 45, 'status': b'The Normal market has closed for 22 MAY 2020.'}, {'exchange': 'CDS', 'length_of_market_type': 6, 'market_type': b'NORMAL', 'length_of_status': 45, 'status': b'The Normal market has closed for 22 MAY 2020.'}, {'exchange': 'BSE', 'length_of_market_type': 13, 'market_type': b'OTHER SESSION', 'length_of_status': 0, 'status': b''}] 351 | ``` 352 | 353 | Note: As per `alice blue` [documentation](http://antplus.aliceblueonline.com/#market-status) all market status messages should be having a timestamp. But in actual the server doesn‘t send timestamp, so the library is unable to get timestamp for now. 354 | 355 | Subscribe to exchange messages 356 | 357 | ```python 358 | sas.subscribe_exchange_messages() 359 | ``` 360 | 361 | Getting market status messages. 362 | 363 | ```python 364 | print(sas.get_exchange_messages()) 365 | ``` 366 | 367 | Example result of `get_exchange_messages()` 368 | 369 | ``` 370 | [{'exchange': 'NSE', 'length': 32, 'message': b'DS : Bulk upload can be started.', 'exchange_time_stamp': 1590148595}, {'exchange': 'NFO', 'length': 200, 'message': b'MARKET WIDE LIMIT FOR VEDL IS 183919959. OPEN POSITIONS IN VEDL HAVE REACHED 84 PERCENT OF THE MARKET WIDE LIMIT. ', 'exchange_time_stamp': 1590146132}, {'exchange': 'CDS', 'length': 54, 'message': b'DS : Regular segment Bhav copy broadcast successfully.', 'exchange_time_stamp': 1590148932}, {'exchange': 'MCX', 'length': 7, 'message': b'.......', 'exchange_time_stamp': 1590196159}] 371 | ``` 372 | 373 | #### Market Status messages & Exchange messages through callbacks 374 | 375 | ```python 376 | socket_opened = False 377 | def market_status_messages(message): 378 | print(f"market status messages {message}") 379 | 380 | def exchange_messages(message): 381 | print(f"exchange messages {message}") 382 | 383 | def open_callback(): 384 | global socket_opened 385 | socket_opened = True 386 | 387 | sas.start_websocket(market_status_messages_callback=market_status_messages, 388 | exchange_messages_callback=exchange_messages, 389 | socket_open_callback=open_callback, 390 | run_in_background=True) 391 | while(socket_opened==False): 392 | pass 393 | sas.subscribe_market_status_messages() 394 | sas.subscribe_exchange_messages() 395 | sleep(10) 396 | ``` 397 | 398 | ### Place an order 399 | 400 | Place limit, market, SL, SL-M, AMO, BO, CO orders 401 | 402 | ```python 403 | print (sas.get_profile()) 404 | 405 | # TransactionType.Buy, OrderType.Market, ProductType.Delivery 406 | 407 | print ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%1%%%%%%%%%%%%%%%%%%%%%%%%%%%%%") 408 | print( 409 | sas.place_order(transaction_type = TransactionType.Buy, 410 | instrument = sas.get_instrument_by_symbol('NSE', 'INFY'), 411 | quantity = 1, 412 | order_type = OrderType.Market, 413 | product_type = ProductType.Delivery, 414 | price = 0.0, 415 | trigger_price = None, 416 | stop_loss = None, 417 | square_off = None, 418 | trailing_sl = None, 419 | is_amo = False) 420 | ) 421 | 422 | # TransactionType.Buy, OrderType.Market, ProductType.Intraday 423 | 424 | print ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%2%%%%%%%%%%%%%%%%%%%%%%%%%%%%%") 425 | print( 426 | sas.place_order(transaction_type = TransactionType.Buy, 427 | instrument = sas.get_instrument_by_symbol('NSE', 'INFY'), 428 | quantity = 1, 429 | order_type = OrderType.Market, 430 | product_type = ProductType.Intraday, 431 | price = 0.0, 432 | trigger_price = None, 433 | stop_loss = None, 434 | square_off = None, 435 | trailing_sl = None, 436 | is_amo = False) 437 | ) 438 | 439 | # TransactionType.Buy, OrderType.Market, ProductType.CoverOrder 440 | 441 | print ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%3%%%%%%%%%%%%%%%%%%%%%%%%%%%%%") 442 | print( 443 | sas.place_order(transaction_type = TransactionType.Buy, 444 | instrument = sas.get_instrument_by_symbol('NSE', 'INFY'), 445 | quantity = 1, 446 | order_type = OrderType.Market, 447 | product_type = ProductType.CoverOrder, 448 | price = 0.0, 449 | trigger_price = 7.5, # trigger_price Here the trigger_price is taken as stop loss (provide stop loss in actual amount) 450 | stop_loss = None, 451 | square_off = None, 452 | trailing_sl = None, 453 | is_amo = False) 454 | ) 455 | 456 | 457 | # TransactionType.Buy, OrderType.Limit, ProductType.BracketOrder 458 | # OCO Order can't be of type market 459 | 460 | print ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%4%%%%%%%%%%%%%%%%%%%%%%%%%%%%%") 461 | print( 462 | sas.place_order(transaction_type = TransactionType.Buy, 463 | instrument = sas.get_instrument_by_symbol('NSE', 'INFY'), 464 | quantity = 1, 465 | order_type = OrderType.Limit, 466 | product_type = ProductType.BracketOrder, 467 | price = 8.0, 468 | trigger_price = None, 469 | stop_loss = 6.0, 470 | square_off = 10.0, 471 | trailing_sl = None, 472 | is_amo = False) 473 | ) 474 | 475 | # TransactionType.Buy, OrderType.Limit, ProductType.Intraday 476 | 477 | print ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%5%%%%%%%%%%%%%%%%%%%%%%%%%%%%%") 478 | print( 479 | sas.place_order(transaction_type = TransactionType.Buy, 480 | instrument = sas.get_instrument_by_symbol('NSE', 'INFY'), 481 | quantity = 1, 482 | order_type = OrderType.Limit, 483 | product_type = ProductType.Intraday, 484 | price = 8.0, 485 | trigger_price = None, 486 | stop_loss = None, 487 | square_off = None, 488 | trailing_sl = None, 489 | is_amo = False) 490 | ) 491 | 492 | 493 | # TransactionType.Buy, OrderType.Limit, ProductType.CoverOrder 494 | 495 | print ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%6%%%%%%%%%%%%%%%%%%%%%%%%%%%%%") 496 | print( 497 | sas.place_order(transaction_type = TransactionType.Buy, 498 | instrument = sas.get_instrument_by_symbol('NSE', 'INFY'), 499 | quantity = 1, 500 | order_type = OrderType.Limit, 501 | product_type = ProductType.CoverOrder, 502 | price = 7.0, 503 | trigger_price = 6.5, # trigger_price Here the trigger_price is taken as stop loss (provide stop loss in actual amount) 504 | stop_loss = None, 505 | square_off = None, 506 | trailing_sl = None, 507 | is_amo = False) 508 | ) 509 | 510 | ############################### 511 | 512 | # TransactionType.Buy, OrderType.StopLossMarket, ProductType.Delivery 513 | 514 | print ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%7%%%%%%%%%%%%%%%%%%%%%%%%%%%%%") 515 | print( 516 | sas.place_order(transaction_type = TransactionType.Buy, 517 | instrument = sas.get_instrument_by_symbol('NSE', 'INFY'), 518 | quantity = 1, 519 | order_type = OrderType.StopLossMarket, 520 | product_type = ProductType.Delivery, 521 | price = 0.0, 522 | trigger_price = 8.0, 523 | stop_loss = None, 524 | square_off = None, 525 | trailing_sl = None, 526 | is_amo = False) 527 | ) 528 | 529 | 530 | # TransactionType.Buy, OrderType.StopLossMarket, ProductType.Intraday 531 | 532 | print ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%8%%%%%%%%%%%%%%%%%%%%%%%%%%%%%") 533 | print( 534 | sas.place_order(transaction_type = TransactionType.Buy, 535 | instrument = sas.get_instrument_by_symbol('NSE', 'INFY'), 536 | quantity = 1, 537 | order_type = OrderType.StopLossMarket, 538 | product_type = ProductType.Intraday, 539 | price = 0.0, 540 | trigger_price = 8.0, 541 | stop_loss = None, 542 | square_off = None, 543 | trailing_sl = None, 544 | is_amo = False) 545 | ) 546 | 547 | 548 | 549 | # TransactionType.Buy, OrderType.StopLossMarket, ProductType.CoverOrder 550 | # CO order is of type Limit and And Market Only 551 | 552 | # TransactionType.Buy, OrderType.StopLossMarket, ProductType.BO 553 | # BO order is of type Limit and And Market Only 554 | 555 | ################################### 556 | 557 | # TransactionType.Buy, OrderType.StopLossLimit, ProductType.Delivery 558 | 559 | print ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%9%%%%%%%%%%%%%%%%%%%%%%%%%%%%%") 560 | print( 561 | sas.place_order(transaction_type = TransactionType.Buy, 562 | instrument = sas.get_instrument_by_symbol('NSE', 'INFY'), 563 | quantity = 1, 564 | order_type = OrderType.StopLossMarket, 565 | product_type = ProductType.Delivery, 566 | price = 8.0, 567 | trigger_price = 8.0, 568 | stop_loss = None, 569 | square_off = None, 570 | trailing_sl = None, 571 | is_amo = False) 572 | ) 573 | 574 | 575 | # TransactionType.Buy, OrderType.StopLossLimit, ProductType.Intraday 576 | 577 | print ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%10%%%%%%%%%%%%%%%%%%%%%%%%%%%%%") 578 | print( 579 | sas.place_order(transaction_type = TransactionType.Buy, 580 | instrument = sas.get_instrument_by_symbol('NSE', 'INFY'), 581 | quantity = 1, 582 | order_type = OrderType.StopLossLimit, 583 | product_type = ProductType.Intraday, 584 | price = 8.0, 585 | trigger_price = 8.0, 586 | stop_loss = None, 587 | square_off = None, 588 | trailing_sl = None, 589 | is_amo = False) 590 | ) 591 | 592 | 593 | 594 | # TransactionType.Buy, OrderType.StopLossLimit, ProductType.CoverOrder 595 | # CO order is of type Limit and And Market Only 596 | 597 | 598 | # TransactionType.Buy, OrderType.StopLossLimit, ProductType.BracketOrder 599 | 600 | print ("%%%%%%%%%%%%%%%%%%%%%%%%%%%%11%%%%%%%%%%%%%%%%%%%%%%%%%%%%%") 601 | print( 602 | sas.place_order(transaction_type = TransactionType.Buy, 603 | instrument = sas.get_instrument_by_symbol('NSE', 'INFY'), 604 | quantity = 1, 605 | order_type = OrderType.StopLossLimit, 606 | product_type = ProductType.BracketOrder, 607 | price = 8.0, 608 | trigger_price = 8.0, 609 | stop_loss = 1.0, 610 | square_off = 1.0, 611 | trailing_sl = 20, 612 | is_amo = False) 613 | ) 614 | ``` 615 | 616 | ### Place basket order 617 | 618 | Basket order is used to buy or sell group of securities simultaneously. 619 | 620 | ```python 621 | order1 = { "instrument" : sas.get_instrument_by_symbol('NSE', 'INFY'), 622 | "order_type" : OrderType.Market, 623 | "quantity" : 1, 624 | "transaction_type" : TransactionType.Buy, 625 | "product_type" : ProductType.Delivery} 626 | order2 = { "instrument" : sas.get_instrument_by_symbol('NSE', 'SBIN'), 627 | "order_type" : OrderType.Limit, 628 | "quantity" : 2, 629 | "price" : 280.0, 630 | "transaction_type" : TransactionType.Sell, 631 | "product_type" : ProductType.Intraday} 632 | order = [order1, order2] 633 | print(sas.place_basket_order(orders)) 634 | ``` 635 | 636 | ### Cancel an order 637 | 638 | ```python 639 | sas.cancel_order('170713000075481') #Cancel an open order 640 | ``` 641 | 642 | ### Getting order history and trade details 643 | 644 | #### Get order history of a particular order 645 | 646 | ```python 647 | print(sas.get_order_history('170713000075481')) 648 | ``` 649 | 650 | #### Get order history of all orders. 651 | 652 | ```python 653 | print(sas.get_order_history()) 654 | ``` 655 | 656 | #### Get trade book 657 | 658 | ```python 659 | print(sas.get_trade_book()) 660 | ``` 661 | 662 | #### Get historical candles data 663 | 664 | This will provide historical data but **not for current day**. 665 | This returns a `pandas` `DataFrame` object which be used with `pandas_ta` to get various indicators values. 666 | 667 | ```python 668 | from datetime import datetime 669 | print(sas.get_historical_candles('MCX', 'NATURALGAS NOV FUT', datetime(2020, 10, 19), datetime.now() ,interval=30)) 670 | ``` 671 | 672 | Output 673 | 674 | ```console 675 | Instrument(exchange='MCX', token=224365, symbol='NATURALGAS NOV FUT', name='', expiry=datetime.date(2020, 11, 24), lot_size=None) 676 | open high low close volume 677 | date 678 | 2020-10-19 09:00:00+05:30 238.9 239.2 238.4 239.0 373 679 | 2020-10-19 09:30:00+05:30 239.0 239.0 238.4 238.6 210 680 | 2020-10-19 10:00:00+05:30 238.7 238.7 238.1 238.1 213 681 | 2020-10-19 10:30:00+05:30 238.0 238.4 238.0 238.1 116 682 | 2020-10-19 11:00:00+05:30 238.1 238.2 238.0 238.0 69 683 | ... ... ... ... ... ... 684 | 2020-10-23 21:00:00+05:30 237.5 238.1 237.3 237.6 331 685 | 2020-10-23 21:30:00+05:30 237.6 238.5 237.6 237.9 754 686 | 2020-10-23 22:00:00+05:30 237.9 238.1 237.2 237.9 518 687 | 2020-10-23 22:30:00+05:30 237.9 238.7 237.7 238.1 897 688 | 2020-10-23 23:00:00+05:30 238.2 238.3 236.3 236.5 1906 689 | 690 | ``` 691 | 692 | Better way to get historical data, first get the latest version from github 693 | 694 | `python -m pip install git+https://github.com/algo2t/alphatrade.git` 695 | 696 | ```python 697 | from datetime import datetime 698 | india_vix_nse_index = sas.get_instrument_by_symbol('NSE', 'India VIX') 699 | print(sas.get_historical_candles(india_vix_nse_index.exchange, india_vix_nse_index.symbol, datetime(2020, 10, 19), datetime.now() ,interval=30)) 700 | ``` 701 | 702 | 703 | #### Get intraday candles data 704 | 705 | This will give candles data for **current day only**. 706 | This returns a `pandas` `DataFrame` object which be used with `pandas_ta` to get various indicators values. 707 | 708 | ```python 709 | print(sas.get_intraday_candles('MCX', 'NATURALGAS NOV FUT', interval=15)) 710 | ``` 711 | 712 | Better way to get intraday data, first get the latest version from github 713 | 714 | `python -m pip install git+https://github.com/algo2t/alphatrade.git` 715 | 716 | ```python 717 | from datetime import datetime 718 | nifty_bank_nse_index = sas.get_instrument_by_symbol('NSE', 'Nifty Bank') 719 | print(sas.get_intraday_candles(nifty_bank_nse_index.exchange, nifty_bank_nse_index.symbol, datetime(2020, 10, 19), datetime.now(), interval=10)) 720 | ``` 721 | 722 | ### Order properties as enums 723 | 724 | Order properties such as TransactionType, OrderType, and others have been safely classified as enums so you don‘t have to write them out as strings 725 | 726 | #### TransactionType 727 | 728 | Transaction types indicate whether you want to buy or sell. Valid transaction types are of the following: 729 | 730 | - `TransactionType.Buy` - buy 731 | - `TransactionType.Sell` - sell 732 | 733 | #### OrderType 734 | 735 | Order type specifies the type of order you want to send. Valid order types include: 736 | 737 | - `OrderType.Market` - Place the order with a market price 738 | - `OrderType.Limit` - Place the order with a limit price (limit price parameter is mandatory) 739 | - `OrderType.StopLossLimit` - Place as a stop loss limit order 740 | - `OrderType.StopLossMarket` - Place as a stop loss market order 741 | 742 | #### ProductType 743 | 744 | Product types indicate the complexity of the order you want to place. Valid product types are: 745 | 746 | - `ProductType.Intraday` - Intraday order that will get squared off before market close 747 | - `ProductType.Delivery` - Delivery order that will be held with you after market close 748 | - `ProductType.CoverOrder` - Cover order 749 | - `ProductType.BracketOrder` - One cancels other order. Also known as bracket order 750 | 751 | ## Working with examples 752 | 753 | [Here](https://github.com/algo2t/alphatrade), examples directory there are 3 files `zlogin_example.py`, `zstreaming_data.py` and `stop.txt` 754 | 755 | ### Steps 756 | 757 | - Clone the repository to your local machine `git clone https://github.com/algo2t/alphatrade.git` 758 | - Copy the examples directory to any location where you want to write your code 759 | - Install the `alphatrade` module using `pip` => `python -m pip install https://github.com/algo2t/alphatrade.git` 760 | - Open the examples directory in your favorite editor, in our case it is [VSCodium](https://vscodium.com/) 761 | - Open the `zlogin_example.py` file in the editor 762 | - Now, create `config.py` file as per instructions given below and in the above file 763 | - Provide correct login credentials like login_id, password and 16 digit totp code (find below qr code) 764 | - This is generally set from the homepage of alpha web trading platform [here](https://alpha.sasonline.in/) 765 | - Click on `FORGET PASSWORD?` => Select `Reset 2FA` radio button. ![image](https://raw.githubusercontent.com/algo2t/alphatrade/main/snaps/forget_password.png) 766 | - Enter the CLIENT ID (LOGIN_ID), EMAIL ID and PAN NUMBER, click on `RESET` button. ![image](https://raw.githubusercontent.com/algo2t/alphatrade/main/snaps/reset_two_fa.png) 767 | - Click on `BACK TO LOGIN` and enter `CLIENT ID` and `PASSWORD`, click on `SECURED SIGN-IN` 768 | - Set same answers for 5 questions and click on `SUBMIT` button. ![image](https://raw.githubusercontent.com/algo2t/alphatrade/main/snaps/set_answers.png) 769 | 770 | `config.py` 771 | ```python 772 | login_id = "XXXXX" 773 | password = "XXXXXXXX" 774 | Totp = 'XXXXXXXXXXXXXXXX' 775 | 776 | try: 777 | access_token = open('access_token.txt', 'r').read().rstrip() 778 | except Exception as e: 779 | print('Exception occurred :: {}'.format(e)) 780 | access_token = None 781 | ``` 782 | 783 | ## Example strategy using alpha trade API 784 | 785 | [Here](https://github.com/algo2t/alphatrade/blob/main/zstreaming_data.py) is an example moving average strategy using alpha trade web API. 786 | This strategy generates a buy signal when 5-EMA > 20-EMA (golden cross) or a sell signal when 5-EMA < 20-EMA (death cross). 787 | 788 | ## Example for getting historical and intraday candles data 789 | 790 | [Here](https://github.com/algo2t/alphatrade/blob/main/zhistorical_data.py) is an example for getting historical data using alpha trade web API. 791 | 792 | For historical candles data `start_time` and `end_time` must be provided in format as shown below. 793 | It can also be provided as `timedelta`. Check the script `zhistorical_data.py` in examples. 794 | 795 | ```python 796 | from datetime import datetime, timedelta 797 | start_time = datetime(2020, 10, 19, 9, 15, 0) 798 | end_time = datetime(2020, 10, 21, 16, 59, 0) 799 | 800 | df = sas.get_historical_candles('MCX', 'NATURALGAS OCT FUT', start_time, end_time, 5) 801 | print(df) 802 | end_time = start_time + timedelta(days=5) 803 | df = sas.get_historical_candles('MCX', 'NATURALGAS NOV FUT', start_time, end_time, 15) 804 | print(df) 805 | ``` 806 | 807 | For intraday or today‘s / current day‘s candles data. 808 | 809 | ```python 810 | df = sas.get_intraday_candles('MCX', 'NATURALGAS OCT FUT') 811 | print(df) 812 | df = sas.get_intraday_candles('MCX', 'NATURALGAS NOV FUT', 15) 813 | print(df) 814 | ``` 815 | 816 | 817 | ## Read this before creating an issue 818 | 819 | Before creating an issue in this library, please follow the following steps. 820 | 821 | 1. Search the problem you are facing is already asked by someone else. There might be some issues already there, either solved/unsolved related to your problem. Go to [issues](https://github.com/algo2t/alphatrade/issues) page, use `is:issue` as filter and search your problem. ![image](https://user-images.githubusercontent.com/38440742/85207058-376ee400-b2f4-11ea-91ad-c8fd8a682a12.png) 822 | 2. If you feel your problem is not asked by anyone or no issues are related to your problem, then create a new issue. 823 | 3. Describe your problem in detail while creating the issue. If you don‘t have time to detail/describe the problem you are facing, assume that I also won‘t be having time to respond to your problem. 824 | 4. Post a sample code of the problem you are facing. If I copy paste the code directly from issue, I should be able to reproduce the problem you are facing. 825 | 5. Before posting the sample code, test your sample code yourself once. Only sample code should be tested, no other addition should be there while you are testing. 826 | 6. Have some print() function calls to display the values of some variables related to your problem. 827 | 7. Post the results of print() functions also in the issue. 828 | 8. Use the insert code feature of github to inset code and print outputs, so that the code is displayed neat. ![image](https://user-images.githubusercontent.com/38440742/85207234-4dc96f80-b2f5-11ea-990c-df013dd69cf2.png) 829 | -------------------------------------------------------------------------------- /alphatrade/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals, absolute_import 2 | 3 | from .alphatrade import AlphaTrade, TransactionType, OrderType, ProductType, LiveFeedType, Instrument 4 | from alphatrade import exceptions 5 | 6 | __all__ = ['AlphaTrade', 'TransactionType', 'OrderType', 7 | 'ProductType', 'LiveFeedType', 'Instrument', 'exceptions'] 8 | -------------------------------------------------------------------------------- /alphatrade/alphatrade.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple, OrderedDict 2 | from datetime import datetime, timedelta 3 | from protlib import CUInt, CStruct, CULong, CUChar, CArray, CUShort, CString 4 | 5 | import alphatrade.exceptions as ex 6 | import enum 7 | import logging 8 | import json 9 | import pandas as pd 10 | import pyotp 11 | import pytz 12 | from time import sleep 13 | import requests 14 | import threading 15 | import websocket 16 | 17 | Instrument = namedtuple('Instrument', ['exchange', 'token', 'symbol', 18 | 'name', 'expiry', 'lot_size']) 19 | logger = logging.getLogger(__name__) 20 | 21 | 22 | class Requests(enum.Enum): 23 | """ Enum for all requests """ 24 | PUT = 1 25 | DELETE = 2 26 | GET = 3 27 | POST = 4 28 | 29 | 30 | class TransactionType(enum.Enum): 31 | Buy = 'BUY' 32 | Sell = 'SELL' 33 | 34 | 35 | class OrderType(enum.Enum): 36 | Market = 'MARKET' 37 | Limit = 'LIMIT' 38 | StopLossLimit = 'SL' 39 | StopLossMarket = 'SL-M' 40 | 41 | 42 | class ProductType(enum.Enum): 43 | Intraday = 'I' 44 | Delivery = 'D' 45 | CoverOrder = 'CO' 46 | BracketOrder = 'BO' 47 | 48 | 49 | class LiveFeedType(enum.Enum): 50 | MARKET_DATA = 1 51 | COMPACT = 2 52 | SNAPQUOTE = 3 53 | FULL_SNAPQUOTE = 4 54 | 55 | 56 | class WsFrameMode(enum.IntEnum): 57 | MARKETDATA = 1 58 | COMPACT_MARKETDATA = 2 59 | SNAPQUOTE = 3 60 | FULL_SNAPQUOTE = 4 61 | SPREADDATA = 5 62 | SPREAD_SNAPQUOTE = 6 63 | DPR = 7 64 | OI = 8 65 | MARKET_STATUS = 9 66 | EXCHANGE_MESSAGES = 10 67 | 68 | 69 | class MarketData(CStruct): 70 | exchange = CUChar() 71 | token = CUInt() 72 | ltp = CUInt() 73 | ltt = CUInt() 74 | ltq = CUInt() 75 | volume = CUInt() 76 | best_bid_price = CUInt() 77 | best_bid_quantity = CUInt() 78 | best_ask_price = CUInt() 79 | best_ask_quantity = CUInt() 80 | total_buy_quantity = CULong() 81 | total_sell_quantity = CULong() 82 | atp = CUInt() 83 | exchange_time_stamp = CUInt() 84 | open = CUInt() 85 | high = CUInt() 86 | low = CUInt() 87 | close = CUInt() 88 | yearly_high = CUInt() 89 | yearly_low = CUInt() 90 | 91 | 92 | class CompactData(CStruct): 93 | exchange = CUChar() 94 | token = CUInt() 95 | ltp = CUInt() 96 | change = CUInt() 97 | exchange_time_stamp = CUInt() 98 | volume = CUInt() 99 | 100 | 101 | class SnapQuote(CStruct): 102 | exchange = CUChar() 103 | token = CUInt() 104 | buyers = CArray(5, CUInt) 105 | bid_prices = CArray(5, CUInt) 106 | bid_quantities = CArray(5, CUInt) 107 | sellers = CArray(5, CUInt) 108 | ask_prices = CArray(5, CUInt) 109 | ask_quantities = CArray(5, CUInt) 110 | exchange_time_stamp = CUInt() 111 | 112 | 113 | class FullSnapQuote(CStruct): 114 | exchange = CUChar() 115 | token = CUInt() 116 | buyers = CArray(5, CUInt) 117 | bid_prices = CArray(5, CUInt) 118 | bid_quantities = CArray(5, CUInt) 119 | sellers = CArray(5, CUInt) 120 | ask_prices = CArray(5, CUInt) 121 | ask_quantities = CArray(5, CUInt) 122 | atp = CUInt() 123 | open = CUInt() 124 | high = CUInt() 125 | low = CUInt() 126 | close = CUInt() 127 | total_buy_quantity = CULong() 128 | total_sell_quantity = CULong() 129 | volume = CUInt() 130 | 131 | 132 | class DPR(CStruct): 133 | exchange = CUChar() 134 | token = CUInt() 135 | exchange_time_stamp = CUInt() 136 | high = CUInt() 137 | low = CUInt() 138 | 139 | 140 | class OpenInterest(CStruct): 141 | exchange = CUChar() 142 | token = CUInt() 143 | current_open_interest = CUChar() 144 | initial_open_interest = CUChar() 145 | exchange_time_stamp = CUInt() 146 | 147 | 148 | class ExchangeMessage(CStruct): 149 | exchange = CUChar() 150 | length = CUShort() 151 | message = CString(length="length") 152 | exchange_time_stamp = CUInt() 153 | 154 | 155 | class MarketStatus(CStruct): 156 | exchange = CUChar() 157 | length_of_market_type = CUShort() 158 | market_type = CString(length="length_of_market_type") 159 | length_of_status = CUShort() 160 | status = CString(length="length_of_status") 161 | 162 | 163 | class AlphaTrade(object): 164 | # dictionary object to hold settings 165 | __service_config = { 166 | 'host': 'https://alpha.sasonline.in', 167 | 'routes': { 168 | 'login': '/api/v3/user/login', 169 | 'validatetotp': '/api/v3/user/validatetotp', 170 | 'profile': '/api/v1/user/profile', 171 | 'master_contract': '/api/v2/contracts.json?exchanges={exchange}', 172 | 'holdings': '/api/v1/holdings', 173 | 'balance': '/api/v1/funds/view?type=all', 174 | 'funds': '/api/v1/funds/view?type={fund_type}', 175 | 'positions_daywise': '/api/v1/positions?type=live', 176 | 'positions_netwise': '/api/v1/positions?type=historical', 177 | 'positions_holdings': '/api/v1/holdings', 178 | 'place_order': '/api/v1/orders', 179 | 'place_amo': '/api/v1/orders/amo', 180 | 'place_bracket_order': '/api/v1/orders/bracket', 181 | 'place_basket_order': '/api/v1/orders/basket', 182 | 'orders': '/api/v1/orders?type={order_type}', 183 | 'get_order_info': '/api/v1/order/{order_id}/history', 184 | 'modify_order': '/api/v1/orders', 185 | 'cancel_order': '/api/v1/order?oms_order_id={order_id}&order_status=open', 186 | 'cancel_bo_order': '/api/v1/order/bracket?oms_order_id={order_id}&order_status=open&leg_order_indicator={leg_order_id}', 187 | 'cancel_co_order': '/api/v1/order/cover?oms_order_id={order_id}&order_status=open&leg_order_indicator={leg_order_id}', 188 | 'trade_book': '/api/v1/trades', 189 | 'scripinfo': '/api/v1/contract/{exchange}?info=scrip&token={token}', 190 | 'contract': '/api/v1/contract/{exchange}', 191 | 'search': '/api/v1/search?key={symbol}', 192 | 'positions': '/api/v1/positions?type={position_type}', 193 | }, 194 | # 'socket_endpoint': 'wss://alpha.sasonline.in/socket/websocket?token={access_token}&login_id={login_id}' 195 | 'socket_endpoint': 'wss://alpha.sasonline.in/hydrasocket/v2/websocket?access_token={access_token}' 196 | } 197 | 198 | _candle_type = {1: 1, 2: 1, 3: 1, 5: 1, 10: 1, 15: 1, 199 | 30: 1, 45: 1, '1H': 2, '2H': 2, '3H': 2, '4H': 2, '1D': 3, 'D': 3, 'W': 3, 'M': 3} 200 | 201 | _data_duration = {1: 1, 2: 2, 3: 3, 5: 5, 10: 10, 15: 15, 202 | 30: 30, 45: 45, '1H': None, '2H': 2, '3H': 2, '4H': 2, '1D': None, 'D': None, 'W': None, 'M': None} 203 | 204 | def __init__(self, login_id, password, twofa, access_token=None, master_contracts_to_download=None): 205 | """ logs in and gets enabled exchanges and products for user """ 206 | if len(twofa) != 6: 207 | pin = pyotp.TOTP(twofa).now() 208 | twofa = f"{int(pin):06d}" if len(pin) <= 5 else pin 209 | 210 | self.__access_token = access_token 211 | self.__login_id = login_id 212 | self.__password = password 213 | self.__twofa = twofa 214 | self.__websocket = None 215 | self.__websocket_connected = False 216 | self.__ws_mutex = threading.Lock() 217 | self.__on_error = None 218 | self.__on_disconnect = None 219 | self.__on_open = None 220 | self.__subscribe_callback = None 221 | self.__order_update_callback = None 222 | self.__market_status_messages_callback = None 223 | self.__exchange_messages_callback = None 224 | self.__oi_callback = None 225 | self.__dpr_callback = None 226 | self.__subscribers = {} 227 | self.__market_status_messages = [] 228 | self.__exchange_messages = [] 229 | self.__exchange_codes = {'NSE': 1, 230 | 'NFO': 2, 231 | 'CDS': 3, 232 | 'MCX': 4, 233 | 'BSE': 6, 234 | 'BFO': 7} 235 | self.__exchange_price_multipliers = {1: 100, 236 | 2: 100, 237 | 3: 10000000, 238 | 4: 100, 239 | 6: 100, 240 | 7: 100} 241 | 242 | self.__headers = { 243 | 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.102 Safari/537.36' 244 | } 245 | 246 | self.session = requests.session() 247 | 248 | if not self.__is_token_valid(): 249 | self.__get_question_ids() 250 | self.__set_access_token() 251 | else: 252 | self.__headers['X-Authorization-Token'] = self.__access_token 253 | 254 | try: 255 | profile = self.get_profile() 256 | except Exception as exp: 257 | raise ex.PermissionException( 258 | f"Couldn't get profile info with credentials provided :: {exp}") 259 | if (profile['status'] == 'error'): 260 | # Don't know why this error comes, but it safe to proceed further. 261 | if (profile['message'] == 'Not able to retrieve AccountInfoService'): 262 | logger.warning( 263 | "Couldn't get profile info - 'Not able to retrieve AccountInfoService'") 264 | else: 265 | raise ex.PermissionException( 266 | f"Couldn't get profile info '{profile['message']}'") 267 | self.__master_contracts_by_token = {} 268 | self.__master_contracts_by_symbol = {} 269 | if (master_contracts_to_download is None): 270 | # for e in self.__enabled_exchanges: 271 | for e in ['NSE', 'NFO', 'CDS', 'BSE', 'BFO', 'MCX']: 272 | self.__get_master_contract(e) 273 | else: 274 | for e in master_contracts_to_download: 275 | self.__get_master_contract(e) 276 | self.ws_thread = None 277 | 278 | def __is_token_valid(self): 279 | if self.__access_token is None: 280 | try: 281 | self.__access_token = open( 282 | 'access_token.txt', 'r', encoding='utf-8').read().rstrip() 283 | except FileNotFoundError: 284 | print('File not found.') 285 | self.__access_token = None 286 | return False 287 | self.__headers['X-Authorization-Token'] = self.__access_token 288 | profile_url = 'https://alpha.sasonline.in/api/v1/user/profile' 289 | resp = self.session.get(profile_url, headers=self.__headers) 290 | return resp.status_code == 200 and resp.json()['status'] == 'success' 291 | 292 | def __get_question_ids(self): 293 | login_body = { 294 | "device": "web", 295 | 'login_id': self.__login_id, 296 | 'password': self.__password 297 | } 298 | response = self.session.post( 299 | 'https://alpha.sasonline.in/api/v3/user/login', json=login_body) 300 | 301 | if response.status_code != 200: 302 | raise requests.HTTPError(response.text) 303 | if 'error' in response.text: 304 | raise requests.HTTPError(response.text) 305 | twofa_token = response.json()['data']['twofa']['twofa_token'] 306 | self.__twofa_token = twofa_token 307 | 308 | def __set_access_token(self): 309 | twofa_body = {"login_id": self.__login_id, 310 | "twofa_token": self.__twofa_token, 311 | "totp": self.__twofa} 312 | response = self.session.post( 313 | 'https://alpha.sasonline.in/api/v3/user/validatetotp', json=twofa_body) 314 | if response.status_code != 200: 315 | raise requests.HTTPError(response.text) 316 | if 'error' in response.text: 317 | raise requests.HTTPError(response.text) 318 | auth_token = response.json()['data']['auth_token'] 319 | 320 | with open('access_token.txt', 'w', encoding='utf-8') as wr: 321 | wr.write(auth_token) 322 | 323 | self.__access_token = auth_token 324 | self.__headers['X-Authorization-Token'] = self.__access_token 325 | 326 | def __convert_prices(self, dictionary, multiplier): 327 | keys = ['ltp', 328 | 'best_bid_price', 329 | 'best_ask_price', 330 | 'atp', 331 | 'open', 332 | 'high', 333 | 'low', 334 | 'close', 335 | 'yearly_high', 336 | 'yearly_low'] 337 | for key in keys: 338 | if (key in dictionary): 339 | dictionary[key] = dictionary[key]/multiplier 340 | multiple_value_keys = ['bid_prices', 'ask_prices'] 341 | for key in multiple_value_keys: 342 | if (key in dictionary): 343 | new_values = [] 344 | for value in dictionary[key]: 345 | new_values.append(value/multiplier) 346 | dictionary[key] = new_values 347 | return dictionary 348 | 349 | def __format_candles(self, data, divider=100): 350 | records = data['data'] 351 | df = pd.DataFrame(records, columns=[ 352 | 'date', 'open', 'high', 'low', 'close', 'volume']) # , index=0) 353 | df['date'] = df['date'].apply( 354 | pd.Timestamp, unit='s', tzinfo=pytz.timezone('Asia/Kolkata')) 355 | # df['datetime'] = df['datetime'].astype(str).str[:-6] 356 | df[['open', 'high', 'low', 'close']] = df[[ 357 | 'open', 'high', 'low', 'close']].astype(float).div(divider) 358 | df['volume'] = df['volume'].astype(int) 359 | df.set_index('date', inplace=True) 360 | return df 361 | 362 | def __convert_exchanges(self, dictionary): 363 | if ('exchange' in dictionary): 364 | d = self.__exchange_codes 365 | dictionary['exchange'] = list(d.keys())[list( 366 | d.values()).index(dictionary['exchange'])] 367 | return dictionary 368 | 369 | def __convert_instrument(self, dictionary): 370 | if ('exchange' in dictionary) and ('token' in dictionary): 371 | dictionary['instrument'] = self.get_instrument_by_token( 372 | dictionary['exchange'], dictionary['token']) 373 | return dictionary 374 | 375 | def __modify_human_readable_values(self, dictionary): 376 | dictionary = self.__convert_prices( 377 | dictionary, self.__exchange_price_multipliers[dictionary['exchange']]) 378 | dictionary = self.__convert_exchanges(dictionary) 379 | dictionary = self.__convert_instrument(dictionary) 380 | return dictionary 381 | 382 | def __on_data_callback(self, ws=None, message=None): 383 | # This workaround is to solve the websocket_client's compatibility 384 | # issue of older versions. ie.0.40.0 which is used in Upstox. 385 | # Now this will work in both 0.40.0 & newer version of websocket_client 386 | if not isinstance(ws, websocket.WebSocketApp): 387 | message = ws 388 | if (message[0] == WsFrameMode.MARKETDATA): 389 | p = MarketData.parse(message[1:]).__dict__ 390 | res = self.__modify_human_readable_values(p) 391 | if (self.__subscribe_callback is not None): 392 | self.__subscribe_callback(res) 393 | elif (message[0] == WsFrameMode.COMPACT_MARKETDATA): 394 | p = CompactData.parse(message[1:]).__dict__ 395 | res = self.__modify_human_readable_values(p) 396 | if (self.__subscribe_callback is not None): 397 | self.__subscribe_callback(res) 398 | elif (message[0] == WsFrameMode.SNAPQUOTE): 399 | p = SnapQuote.parse(message[1:]).__dict__ 400 | res = self.__modify_human_readable_values(p) 401 | if (self.__subscribe_callback is not None): 402 | self.__subscribe_callback(res) 403 | elif (message[0] == WsFrameMode.FULL_SNAPQUOTE): 404 | p = FullSnapQuote.parse(message[1:]).__dict__ 405 | res = self.__modify_human_readable_values(p) 406 | if (self.__subscribe_callback is not None): 407 | self.__subscribe_callback(res) 408 | elif (message[0] == WsFrameMode.DPR): 409 | p = DPR.parse(message[1:]).__dict__ 410 | res = self.__modify_human_readable_values(p) 411 | if (self.__dpr_callback is not None): 412 | self.__dpr_callback(res) 413 | elif (message[0] == WsFrameMode.OI): 414 | p = OpenInterest.parse(message[1:]).__dict__ 415 | res = self.__modify_human_readable_values(p) 416 | if (self.__oi_callback is not None): 417 | self.__oi_callback(res) 418 | elif (message[0] == WsFrameMode.MARKET_STATUS): 419 | p = MarketStatus.parse(message[1:]).__dict__ 420 | res = self.__modify_human_readable_values(p) 421 | self.__market_status_messages.append(res) 422 | if (self.__market_status_messages_callback is not None): 423 | self.__market_status_messages_callback(res) 424 | elif (message[0] == WsFrameMode.EXCHANGE_MESSAGES): 425 | p = ExchangeMessage.parse(message[1:]).__dict__ 426 | res = self.__modify_human_readable_values(p) 427 | self.__exchange_messages.append(res) 428 | if (self.__exchange_messages_callback is not None): 429 | self.__exchange_messages_callback(res) 430 | 431 | def __on_close_callback(self, ws=None): 432 | self.__websocket_connected = False 433 | if self.__on_disconnect: 434 | self.__on_disconnect() 435 | 436 | def __on_open_callback(self, ws=None): 437 | self.__websocket_connected = True 438 | self.__resubscribe() 439 | if self.__on_open: 440 | self.__on_open() 441 | 442 | def __on_error_callback(self, ws=None, error=None): 443 | # This workaround is to solve the websocket_client's compatibility issue of older versions. ie.0.40.0 which is used in upstox. Now this will work in both 0.40.0 & newer version of websocket_client 444 | if (isinstance(ws, websocket.WebSocketApp) is not True): 445 | error = ws 446 | if self.__on_error: 447 | self.__on_error(error) 448 | 449 | def __send_heartbeat(self): 450 | heart_beat = {"a": "h", "v": [], "m": ""} 451 | while True: 452 | sleep(5) 453 | self.__ws_send(json.dumps(heart_beat), 454 | opcode=websocket._abnf.ABNF.OPCODE_PING) 455 | 456 | def __ws_run_forever(self): 457 | while True: 458 | try: 459 | self.__websocket.run_forever() 460 | except Exception as exp: 461 | logger.warning( 462 | f"websocket run forever ended in exception, {exp}") 463 | sleep(0.1) # Sleep for 100ms between reconnection. 464 | 465 | def __ws_send(self, *args, **kwargs): 466 | while self.__websocket_connected == False: 467 | # sleep for 50ms if websocket is not connected, wait for reconnection 468 | sleep(0.05) 469 | with self.__ws_mutex: 470 | self.__websocket.send(*args, **kwargs) 471 | 472 | def start_websocket(self, subscribe_callback=None, 473 | order_update_callback=None, 474 | socket_open_callback=None, 475 | socket_close_callback=None, 476 | socket_error_callback=None, 477 | run_in_background=False, 478 | market_status_messages_callback=None, 479 | exchange_messages_callback=None, 480 | oi_callback=None, 481 | dpr_callback=None): 482 | """ Start a websocket connection for getting live data """ 483 | self.__on_open = socket_open_callback 484 | self.__on_disconnect = socket_close_callback 485 | self.__on_error = socket_error_callback 486 | self.__subscribe_callback = subscribe_callback 487 | self.__order_update_callback = order_update_callback 488 | self.__market_status_messages_callback = market_status_messages_callback 489 | self.__exchange_messages_callback = exchange_messages_callback 490 | self.__oi_callback = oi_callback 491 | self.__dpr_callback = dpr_callback 492 | 493 | url = self.__service_config['socket_endpoint'].format( 494 | access_token=self.__access_token) 495 | self.__websocket = websocket.WebSocketApp(url, 496 | on_data=self.__on_data_callback, 497 | on_error=self.__on_error_callback, 498 | on_close=self.__on_close_callback, 499 | on_open=self.__on_open_callback) 500 | th = threading.Thread(target=self.__send_heartbeat) 501 | th.daemon = True 502 | th.start() 503 | if run_in_background is True: 504 | self.__ws_thread = threading.Thread(target=self.__ws_run_forever) 505 | self.__ws_thread.daemon = True 506 | self.__ws_thread.start() 507 | else: 508 | self.__ws_run_forever() 509 | 510 | def get_profile(self): 511 | """ Get profile """ 512 | profile = self.__api_call_helper('profile', Requests.GET, None, None) 513 | if (profile['status'] != 'error'): 514 | # self.__enabled_exchanges = profile['data']['exchanges_subscribed'] 515 | self.__enabled_exchanges = ['NSE', 'MCX', 'NFO', 'BSE', 'BFO', 'CDS'] 516 | return profile 517 | 518 | def get_balance(self): 519 | """ Get balance/margins """ 520 | return self.__api_call_helper('balance', Requests.GET, None, None) 521 | 522 | def get_daywise_positions(self): 523 | """ Get daywise positions """ 524 | return self.__api_call_helper('positions_daywise', Requests.GET, None, None) 525 | 526 | def get_netwise_positions(self): 527 | """ Get netwise positions """ 528 | return self.__api_call_helper('positions_netwise', Requests.GET, None, None) 529 | 530 | def get_holding_positions(self): 531 | """ Get holding positions """ 532 | return self.__api_call_helper('positions_holdings', Requests.GET, None, None) 533 | 534 | def positions(self, position_type='live'): 535 | """ get all positions """ 536 | params = {'position_type': position_type} 537 | return self.__api_call_helper('positions', Requests.GET, params, None) 538 | 539 | def orders(self, order_type='complete'): 540 | """ get all orders """ 541 | params = {'order_type': order_type} 542 | return self.__api_call_helper('orders', Requests.GET, params, None) 543 | 544 | def funds(self, fund_type='all'): 545 | """ get all funds """ 546 | params = {'fund_type': fund_type} 547 | return self.__api_call_helper('funds', Requests.GET, params, None) 548 | 549 | # def __to_instrument(self, scrip): 550 | # """ Convert scrip to instrument """ 551 | # print(scrip) 552 | # instrument = Instrument(scrip['exchange'], int(scrip['token']), 553 | # scrip['trading_symbol'], 554 | # scrip['company'], None, 1) 555 | # print(isinstance(instrument, Instrument)) 556 | # print(instrument) 557 | # return instrument 558 | 559 | 560 | def search(self, symbol='Nifty Bank', exchange='NSE'): 561 | """ search for scrips """ 562 | params = {'symbol': symbol} 563 | data = self.__api_call_helper('search', Requests.GET, params, None) 564 | return data['result'] 565 | 566 | def get_order_history(self, order_id=None): 567 | """ leave order_id as None to get all entire order history """ 568 | if order_id is None: 569 | return self.orders() 570 | else: 571 | return self.__api_call_helper('get_order_info', Requests.GET, {'order_id': order_id}, None) 572 | 573 | def get_scrip_info(self, instrument): 574 | """ Get scrip information """ 575 | params = {'exchange': instrument.exchange, 'token': instrument.token} 576 | return self.__api_call_helper('scripinfo', Requests.GET, params, None) 577 | 578 | def get_trade_book(self): 579 | """ get all trades """ 580 | return self.__api_call_helper('trade_book', Requests.GET, None, None) 581 | 582 | def get_exchanges(self): 583 | """ Get enabled exchanges """ 584 | return self.__enabled_exchanges 585 | 586 | def __get_product_type_str(self, product_type, exchange): 587 | prod_type = None 588 | if (product_type == ProductType.Intraday): 589 | prod_type = 'MIS' 590 | elif product_type == ProductType.Delivery: 591 | prod_type = 'NRML' if exchange in ['NFO', 'MCX', 'CDS'] else 'CNC' 592 | elif (product_type == ProductType.CoverOrder): 593 | prod_type = 'CO' 594 | elif (product_type == ProductType.BracketOrder): 595 | prod_type = None 596 | return prod_type 597 | 598 | def place_order(self, transaction_type, instrument, quantity, order_type, 599 | product_type, price=0.0, trigger_price=None, 600 | stop_loss=None, square_off=None, trailing_sl=None, 601 | is_amo=False, 602 | order_tag='python'): 603 | """ placing an order, many fields are optional and are not required 604 | for all order types 605 | """ 606 | if transaction_type is None: 607 | raise TypeError( 608 | "Required parameter transaction_type not of type TransactionType") 609 | 610 | if not isinstance(instrument, Instrument): 611 | raise TypeError( 612 | "Required parameter instrument not of type Instrument") 613 | 614 | if not isinstance(quantity, int): 615 | raise TypeError("Required parameter quantity not of type int") 616 | 617 | if order_type is None: 618 | raise TypeError( 619 | "Required parameter order_type not of type OrderType") 620 | 621 | if product_type is None: 622 | raise TypeError( 623 | "Required parameter product_type not of type ProductType") 624 | 625 | if price is not None and not isinstance(price, float): 626 | raise TypeError("Optional parameter price not of type float") 627 | 628 | if trigger_price is not None and not isinstance(trigger_price, float): 629 | raise TypeError( 630 | "Optional parameter trigger_price not of type float") 631 | 632 | prod_type = self.__get_product_type_str( 633 | product_type, instrument.exchange) 634 | # construct order object after all required parameters are met 635 | order = {'exchange': instrument.exchange, 636 | 'order_type': order_type.value, 637 | 'instrument_token': instrument.token, 638 | 'quantity': quantity, 639 | 'disclosed_quantity': 0, 640 | 'price': price, 641 | 'transaction_type': transaction_type.value, 642 | 'trigger_price': trigger_price, 643 | 'validity': 'DAY', 644 | 'product': prod_type, 645 | 'source': 'web', 646 | 'order_tag': order_tag} 647 | 648 | if stop_loss is not None: 649 | if isinstance(stop_loss, float): 650 | order['stop_loss_value'] = stop_loss 651 | 652 | else: 653 | raise TypeError( 654 | "Optional parameter stop_loss not of type float") 655 | if square_off is not None: 656 | if isinstance(square_off, float): 657 | order['square_off_value'] = square_off 658 | 659 | else: 660 | raise TypeError( 661 | "Optional parameter square_off not of type float") 662 | if trailing_sl is not None: 663 | if not isinstance(trailing_sl, int): 664 | raise TypeError( 665 | "Optional parameter trailing_sl not of type int") 666 | else: 667 | order['trailing_stop_loss'] = trailing_sl 668 | 669 | if product_type is ProductType.CoverOrder and not isinstance( 670 | trigger_price, float 671 | ): 672 | raise TypeError( 673 | "Required parameter trigger_price not of type float") 674 | 675 | helper = 'place_amo' if (is_amo == True) else 'place_order' 676 | if product_type is ProductType.BracketOrder: 677 | helper = 'place_bracket_order' 678 | del order['product'] 679 | if not isinstance(stop_loss, float): 680 | raise TypeError( 681 | "Required parameter stop_loss not of type float") 682 | 683 | if not isinstance(square_off, float): 684 | raise TypeError( 685 | "Required parameter square_off not of type float") 686 | 687 | return self.__api_call_helper(helper, Requests.POST, None, order) 688 | 689 | def place_basket_order(self, orders): 690 | """ placing a basket order, 691 | Argument orders should be a list of all orders that should be sent 692 | each element in order should be a dictionary containing the following key. 693 | "instrument", "order_type", "quantity", "price" (only if its a limit order), 694 | "transaction_type", "product_type" 695 | """ 696 | keys = {"instrument": Instrument, 697 | "order_type": OrderType, 698 | "quantity": int, 699 | "transaction_type": TransactionType, 700 | "product_type": ProductType} 701 | if not isinstance(orders, list): 702 | raise TypeError("Required parameter orders is not of type list") 703 | 704 | if len(orders) <= 0: 705 | raise TypeError("Length of orders should be greater than 0") 706 | 707 | for i in orders: 708 | if not isinstance(i, dict): 709 | raise TypeError( 710 | "Each element in orders should be of type dict") 711 | for s, value in keys.items(): 712 | if s not in i: 713 | raise TypeError( 714 | f"Each element in orders should have key {s}") 715 | if type(i[s]) is not value: 716 | raise TypeError( 717 | f"Element '{s}' in orders should be of type {keys[s]}") 718 | if i['order_type'] == OrderType.Limit: 719 | if "price" not in i: 720 | raise TypeError( 721 | "Each element in orders should have key 'price' if its a limit order ") 722 | if not isinstance(i['price'], float): 723 | raise TypeError( 724 | "Element price in orders should be of type float") 725 | else: 726 | i['price'] = 0.0 727 | if i['order_type'] in [ 728 | OrderType.StopLossLimit, 729 | OrderType.StopLossMarket, 730 | ]: 731 | if 'trigger_price' not in i: 732 | raise TypeError( 733 | f"Each element in orders should have key 'trigger_price' if it is an {i['order_type']} order") 734 | if not isinstance(i['trigger_price'], float): 735 | raise TypeError( 736 | "Element trigger_price in orders should be of type float") 737 | else: 738 | i['trigger_price'] = 0.0 739 | if (i['product_type'] == ProductType.Intraday): 740 | i['product_type'] = 'MIS' 741 | elif i['product_type'] == ProductType.Delivery: 742 | i['product_type'] = 'NRML' if ( 743 | i['instrument'].exchange == 'NFO') else 'CNC' 744 | elif i['product_type'] in [ 745 | ProductType.CoverOrder, 746 | ProductType.BracketOrder, 747 | ]: 748 | raise TypeError( 749 | "Product Type BO or CO is not supported in basket order") 750 | if i['quantity'] <= 0: 751 | raise TypeError("Quantity should be greater than 0") 752 | 753 | data = {'source': 'web', 754 | 'orders': []} 755 | for i in orders: 756 | # construct order object after all required parameters are met 757 | data['orders'].append({'exchange': i['instrument'].exchange, 758 | 'order_type': i['order_type'].value, 759 | 'instrument_token': i['instrument'].token, 760 | 'quantity': i['quantity'], 761 | 'disclosed_quantity': 0, 762 | 'price': i['price'], 763 | 'transaction_type': i['transaction_type'].value, 764 | 'trigger_price': i['trigger_price'], 765 | 'validity': 'DAY', 766 | 'product': i['product_type']}) 767 | 768 | helper = 'place_basket_order' 769 | return self.__api_call_helper(helper, Requests.POST, None, data) 770 | 771 | def modify_order(self, transaction_type, instrument, product_type, order_id, order_type, quantity=None, price=0.0, 772 | trigger_price=0.0): 773 | """ modify an order, transaction_type, instrument, product_type, order_id & order_type is required, 774 | rest are optional, use only when when you want to change that attribute. 775 | """ 776 | if not isinstance(instrument, Instrument): 777 | raise TypeError( 778 | "Required parameter instrument not of type Instrument") 779 | 780 | if not isinstance(order_id, str): 781 | raise TypeError("Required parameter order_id not of type str") 782 | 783 | if quantity is not None and not isinstance(quantity, int): 784 | raise TypeError("Optional parameter quantity not of type int") 785 | 786 | if type(order_type) is not OrderType: 787 | raise TypeError( 788 | "Optional parameter order_type not of type OrderType") 789 | 790 | if ProductType is None: 791 | raise TypeError( 792 | "Required parameter product_type not of type ProductType") 793 | 794 | if price is not None and not isinstance(price, float): 795 | raise TypeError("Optional parameter price not of type float") 796 | 797 | if trigger_price is not None and not isinstance(trigger_price, float): 798 | raise TypeError( 799 | "Optional parameter trigger_price not of type float") 800 | 801 | if (product_type == ProductType.Intraday): 802 | product_type = 'MIS' 803 | elif product_type == ProductType.Delivery: 804 | product_type = 'NRML' if (instrument.exchange == 'NFO') else 'CNC' 805 | elif (product_type == ProductType.CoverOrder): 806 | product_type = 'CO' 807 | elif (product_type == ProductType.BracketOrder): 808 | product_type = None 809 | # construct order object with order id 810 | order = {'oms_order_id': str(order_id), 811 | 'instrument_token': int(instrument.token), 812 | 'exchange': instrument.exchange, 813 | 'transaction_type': transaction_type.value, 814 | 'product': product_type, 815 | 'validity': 'DAY', 816 | 'order_type': order_type.value, 817 | 'price': price, 818 | 'trigger_price': trigger_price, 819 | 'quantity': quantity, 820 | 'disclosed_quantity': 0, 821 | 'nest_request_id': '1'} 822 | return self.__api_call_helper('modify_order', Requests.PUT, None, order) 823 | 824 | def cancel_order(self, order_id, leg_order_id=None, is_co=False): 825 | """ Cancel single order """ 826 | if (is_co == False): 827 | if leg_order_id is None: 828 | ret = self.__api_call_helper('cancel_order', Requests.DELETE, { 829 | 'order_id': order_id}, None) 830 | else: 831 | ret = self.__api_call_helper('cancel_bo_order', Requests.DELETE, { 832 | 'order_id': order_id, 'leg_order_id': leg_order_id}, None) 833 | else: 834 | ret = self.__api_call_helper('cancel_co_order', Requests.DELETE, { 835 | 'order_id': order_id, 'leg_order_id': leg_order_id}, None) 836 | return ret 837 | 838 | def cancel_all_orders(self): 839 | """ Cancel all orders """ 840 | ret = [] 841 | orders = self.get_order_history()['data'] 842 | if not orders: 843 | return 844 | for c_order in orders['pending_orders']: 845 | if (c_order['product'] == 'BO' and c_order['leg_order_indicator']): 846 | r = self.cancel_order( 847 | c_order['leg_order_indicator'], leg_order_id=c_order['leg_order_indicator']) 848 | elif (c_order['product'] == 'CO'): 849 | r = self.cancel_order( 850 | c_order['oms_order_id'], leg_order_id=c_order['leg_order_indicator'], is_co=True) 851 | else: 852 | r = self.cancel_order(c_order['oms_order_id']) 853 | ret.append(r) 854 | return ret 855 | 856 | def subscribe_market_status_messages(self): 857 | """ Subscribe to market messages """ 858 | return self.__ws_send(json.dumps({"a": "subscribe", "v": [1, 2, 3, 4, 6], "m": "market_status"})) 859 | 860 | def get_market_status_messages(self): 861 | """ Get market messages """ 862 | return self.__market_status_messages 863 | 864 | def subscribe_exchange_messages(self): 865 | """ Subscribe to exchange messages """ 866 | return self.__ws_send(json.dumps({"a": "subscribe", "v": [1, 2, 3, 4, 6], "m": "exchange_messages"})) 867 | 868 | def get_exchange_messages(self): 869 | """ Get stored exchange messages """ 870 | return self.__exchange_messages 871 | 872 | def subscribe(self, instrument, live_feed_type): 873 | """ subscribe to the current feed of an instrument """ 874 | if (type(live_feed_type) is not LiveFeedType): 875 | raise TypeError( 876 | "Required parameter live_feed_type not of type LiveFeedType") 877 | arr = [] 878 | if (isinstance(instrument, list)): 879 | for _instrument in instrument: 880 | if not isinstance(_instrument, Instrument): 881 | raise TypeError( 882 | "Required parameter instrument not of type Instrument") 883 | exchange = self.__exchange_codes[_instrument.exchange] 884 | arr.append([exchange, int(_instrument.token)]) 885 | self.__subscribers[_instrument] = live_feed_type 886 | else: 887 | if not isinstance(instrument, Instrument): 888 | raise TypeError( 889 | "Required parameter instrument not of type Instrument") 890 | exchange = self.__exchange_codes[instrument.exchange] 891 | arr = [[exchange, int(instrument.token)]] 892 | self.__subscribers[instrument] = live_feed_type 893 | if (live_feed_type == LiveFeedType.MARKET_DATA): 894 | mode = 'marketdata' 895 | elif (live_feed_type == LiveFeedType.COMPACT): 896 | mode = 'compact_marketdata' 897 | elif (live_feed_type == LiveFeedType.SNAPQUOTE): 898 | mode = 'snapquote' 899 | elif (live_feed_type == LiveFeedType.FULL_SNAPQUOTE): 900 | mode = 'full_snapquote' 901 | data = json.dumps({'a': 'subscribe', 'v': arr, 'm': mode}) 902 | return self.__ws_send(data) 903 | 904 | def unsubscribe(self, instrument, live_feed_type): 905 | """ unsubscribe to the current feed of an instrument """ 906 | if (type(live_feed_type) is not LiveFeedType): 907 | raise TypeError( 908 | "Required parameter live_feed_type not of type LiveFeedType") 909 | arr = [] 910 | if (isinstance(instrument, list)): 911 | for _instrument in instrument: 912 | if not isinstance(_instrument, Instrument): 913 | raise TypeError( 914 | "Required parameter instrument not of type Instrument") 915 | exchange = self.__exchange_codes[_instrument.exchange] 916 | arr.append([exchange, int(_instrument.token)]) 917 | if (_instrument in self.__subscribers): 918 | del self.__subscribers[_instrument] 919 | else: 920 | if not isinstance(instrument, Instrument): 921 | raise TypeError( 922 | "Required parameter instrument not of type Instrument") 923 | exchange = self.__exchange_codes[instrument.exchange] 924 | arr = [[exchange, int(instrument.token)]] 925 | if (instrument in self.__subscribers): 926 | del self.__subscribers[instrument] 927 | if (live_feed_type == LiveFeedType.MARKET_DATA): 928 | mode = 'marketdata' 929 | elif (live_feed_type == LiveFeedType.COMPACT): 930 | mode = 'compact_marketdata' 931 | elif (live_feed_type == LiveFeedType.SNAPQUOTE): 932 | mode = 'snapquote' 933 | elif (live_feed_type == LiveFeedType.FULL_SNAPQUOTE): 934 | mode = 'full_snapquote' 935 | 936 | data = json.dumps({'a': 'unsubscribe', 'v': arr, 'm': mode}) 937 | return self.__ws_send(data) 938 | 939 | def get_all_subscriptions(self): 940 | """ get the all subscribed instruments """ 941 | return self.__subscribers 942 | 943 | def __resubscribe(self): 944 | market = [] 945 | compact = [] 946 | snap = [] 947 | full = [] 948 | for key, value in self.get_all_subscriptions().items(): 949 | if (value == LiveFeedType.MARKET_DATA): 950 | market.append(key) 951 | elif (value == LiveFeedType.COMPACT): 952 | compact.append(key) 953 | elif (value == LiveFeedType.SNAPQUOTE): 954 | snap.append(key) 955 | elif (value == LiveFeedType.FULL_SNAPQUOTE): 956 | full.append(key) 957 | if market: 958 | self.subscribe(market, LiveFeedType.MARKET_DATA) 959 | if compact: 960 | self.subscribe(compact, LiveFeedType.COMPACT) 961 | if snap: 962 | self.subscribe(snap, LiveFeedType.SNAPQUOTE) 963 | if full: 964 | self.subscribe(full, LiveFeedType.FULL_SNAPQUOTE) 965 | 966 | def get_instrument_by_symbol(self, exchange, symbol): 967 | """ get instrument by providing symbol """ 968 | # get instrument given exchange and symbol 969 | exchange = exchange.upper() 970 | # check if master contract exists 971 | if exchange not in self.__master_contracts_by_symbol: 972 | logger.warning(f"Cannot find exchange {exchange} in master contract. " 973 | "Please ensure if that exchange is enabled in your profile and downloaded the master contract for the same") 974 | return None 975 | master_contract = self.__master_contracts_by_symbol[exchange] 976 | if symbol not in master_contract: 977 | logger.warning( 978 | f"Cannot find symbol {exchange} {symbol} in master contract") 979 | return None 980 | return master_contract[symbol] 981 | 982 | def get_instrument_for_fno(self, symbol, expiry_date, is_fut=False, strike=None, is_call=False, exchange='NFO'): 983 | """ get instrument for FNO """ 984 | res = self.search_instruments(exchange, symbol) 985 | if (res == None): 986 | return 987 | matches = [] 988 | for i in res: 989 | sp = i.symbol.split(' ') 990 | if (sp[0] == symbol): 991 | if (i.expiry == expiry_date): 992 | matches.append(i) 993 | for i in matches: 994 | if (is_fut == True): 995 | if ('FUT' in i.symbol): 996 | return i 997 | else: 998 | sp = i.symbol.split(' ') 999 | if ((sp[-1] == 'CE') or (sp[-1] == 'PE')): # Only option scrips 1000 | if (float(sp[-2]) == float(strike)): 1001 | if (is_call == True): 1002 | if (sp[-1] == 'CE'): 1003 | return i 1004 | else: 1005 | if (sp[-1] == 'PE'): 1006 | return i 1007 | 1008 | def search_instruments(self, exchange, symbol): 1009 | """ Search instrument by symbol match """ 1010 | # search instrument given exchange and symbol 1011 | exchange = exchange.upper() 1012 | matches = [] 1013 | # check if master contract exists 1014 | if exchange not in self.__master_contracts_by_token: 1015 | logger.warning(f"Cannot find exchange {exchange} in master contract. " 1016 | "Please ensure if that exchange is enabled in your profile and downloaded the master contract for the same") 1017 | return None 1018 | master_contract = self.__master_contracts_by_token[exchange] 1019 | for contract in master_contract: 1020 | if (isinstance(symbol, list)): 1021 | for sym in symbol: 1022 | if sym.lower() in master_contract[contract].symbol.split(' ')[0].lower(): 1023 | matches.append(master_contract[contract]) 1024 | else: 1025 | if symbol.lower() in master_contract[contract].symbol.split(' ')[0].lower(): 1026 | matches.append(master_contract[contract]) 1027 | return matches 1028 | 1029 | def get_instrument_by_token(self, exchange, token): 1030 | """ Get instrument by providing token """ 1031 | # get instrument given exchange and token 1032 | exchange = exchange.upper() 1033 | token = int(token) 1034 | 1035 | 1036 | def get_master_contract(self, exchange): 1037 | """ Get master contract """ 1038 | return self.__master_contracts_by_symbol[exchange] 1039 | 1040 | def __get_master_contract(self, exchange): 1041 | """ returns all the tradable contracts of an exchange 1042 | placed in an OrderedDict and the key is the token 1043 | """ 1044 | print(f'Downloading master contracts for exchange: {exchange}') 1045 | body = self.__api_call_helper( 1046 | 'master_contract', Requests.GET, {'exchange': exchange}, None) 1047 | master_contract_by_token = OrderedDict() 1048 | master_contract_by_symbol = OrderedDict() 1049 | for sub in body: 1050 | for scrip in body[sub]: 1051 | # convert token 1052 | token = int(scrip['code']) 1053 | 1054 | # convert symbol to upper 1055 | symbol = scrip['symbol'] 1056 | 1057 | # convert expiry to none if it's non-existent 1058 | if ('expiry' in scrip): 1059 | expiry = datetime.fromtimestamp( 1060 | scrip['expiry']).date() 1061 | else: 1062 | expiry = None 1063 | 1064 | # convert lot size to int 1065 | lot_size = scrip['lotSize'] if ('lotSize' in scrip) else None 1066 | # Name & Exchange 1067 | name = scrip['company'] 1068 | exch = scrip['exchange'] 1069 | 1070 | instrument = Instrument( 1071 | exch, token, symbol, name, expiry, lot_size) 1072 | master_contract_by_token[token] = instrument 1073 | master_contract_by_symbol[symbol] = instrument 1074 | self.__master_contracts_by_token[exchange] = master_contract_by_token 1075 | self.__master_contracts_by_symbol[exchange] = master_contract_by_symbol 1076 | 1077 | def __api_call_helper(self, name, http_method, params, data): 1078 | # helper formats the url and reads error codes nicely 1079 | config = self.__service_config 1080 | url = f"{config['host']}{config['routes'][name]}" 1081 | if params is not None: 1082 | url = url.format(**params) 1083 | 1084 | response = self.__api_call(url, http_method, data) 1085 | if response.status_code != 200: 1086 | raise requests.HTTPError(response.text) 1087 | return json.loads(response.text) 1088 | 1089 | def __api_call(self, url, http_method, data): 1090 | # logger.debug('url:: %s http_method:: %s data:: %s headers:: %s', url, http_method, data, headers) 1091 | headers = {"Content-Type": "application/json"} 1092 | if (len(self.__access_token) > 100): 1093 | headers['X-Authorization-Token'] = self.__access_token 1094 | headers['Connection'] = 'keep-alive' 1095 | else: 1096 | headers['client_id'] = self.__login_id 1097 | headers['authorization'] = f"Bearer {self.__access_token}" 1098 | r = None 1099 | if http_method is Requests.POST: 1100 | r = self.session.post( 1101 | url, data=json.dumps(data), headers=headers) 1102 | elif http_method is Requests.DELETE: 1103 | r = self.session.delete(url, headers=headers) 1104 | elif http_method is Requests.PUT: 1105 | r = self.session.put( 1106 | url, data=json.dumps(data), headers=headers) 1107 | elif http_method is Requests.GET: 1108 | params = {'client_id': self.__login_id} 1109 | r = self.session.get(url, params=params, headers=headers) 1110 | return r 1111 | 1112 | def get_historical_candles(self, exchange, symbol, start_time, end_time, interval=5, is_index=False): 1113 | exchange = exchange.upper() 1114 | idx = '' if not is_index else '_INDICES' 1115 | divider = 100 1116 | if exchange == 'CDS': 1117 | divider = 1e7 1118 | # symbol = symbol.upper() 1119 | instrument = self.get_instrument_by_symbol(exchange, symbol) 1120 | print(instrument) 1121 | start_time = int(start_time.timestamp()) 1122 | end_time = int(end_time.timestamp()) 1123 | 1124 | PARAMS = { 1125 | 'candletype': 1, 1126 | 'data_duration': interval, 1127 | 'starttime': start_time, 1128 | 'endtime': end_time, 1129 | 'exchange': exchange + idx, 1130 | 'type': 'historical', 1131 | 'token': instrument.token 1132 | } 1133 | 1134 | r = self.session.get( 1135 | 'https://alpha.sasonline.in/api/v1/charts', params=PARAMS, headers=self.__headers) 1136 | data = r.json() 1137 | return self.__format_candles(data, divider) 1138 | 1139 | def get_intraday_candles(self, exchange, symbol, interval=5): 1140 | exchange = exchange.upper() 1141 | divider = 100 1142 | if exchange == 'CDS': 1143 | divider = 1e7 1144 | # symbol = symbol.upper() 1145 | today = datetime.today() 1146 | start_time = int(datetime(today.year, today.month, 1147 | today.day, hour=9, minute=00).timestamp()) 1148 | 1149 | previous_interval_minute = datetime.now().minute // interval * interval 1150 | 1151 | end_time = int(datetime(today.year, today.month, 1152 | today.day, hour=today.hour, minute=previous_interval_minute).timestamp()) 1153 | 1154 | instrument = self.get_instrument_by_symbol(exchange, symbol) 1155 | 1156 | PARAMS = { 1157 | 'candletype': 1, 1158 | 'data_duration': interval, 1159 | 'starttime': start_time, 1160 | 'endtime': end_time, 1161 | 'exchange': exchange, 1162 | 'type': 'live', 1163 | 'token': instrument.token 1164 | } 1165 | 1166 | r = self.session.get( 1167 | 'https://alpha.sasonline.in/api/v1/charts', params=PARAMS, headers=self.__headers) 1168 | data = r.json() 1169 | return self.__format_candles(data, divider) 1170 | 1171 | def buy_bo(self, instrument, qty, price, trigger_price, stop_loss_value, square_off_value): 1172 | data = self.place_order(TransactionType.Buy, instrument, qty, 1173 | OrderType.StopLossLimit, ProductType.BracketOrder, 1174 | price=price, trigger_price=trigger_price, stop_loss=stop_loss_value, 1175 | square_off=square_off_value, order_tag='python-buy-bo') 1176 | if data['status'] == 'success': 1177 | return data['data']['oms_order_id'] 1178 | return data.json() 1179 | 1180 | def sell_bo(self, instrument, qty, price, trigger_price, stop_loss_value, square_off_value): 1181 | data = self.place_order(TransactionType.Sell, instrument, qty, 1182 | OrderType.StopLossLimit, ProductType.BracketOrder, 1183 | price=price, trigger_price=trigger_price, stop_loss=stop_loss_value, 1184 | square_off=square_off_value, order_tag='python-sell-bo') 1185 | if data['status'] == 'success': 1186 | return data['data']['oms_order_id'] 1187 | return data.json() 1188 | 1189 | def get_total_m2m(self): 1190 | """ Returns the total m2m of all positions. 1191 | Returns: 1192 | float: Total m2m of all positions. 1193 | """ 1194 | data = self.positions(position_type='live') 1195 | if data['status'] != 'success': 1196 | return None 1197 | else: 1198 | positions = pd.DataFrame(data['data']['positions'], index=None) 1199 | if positions.empty: 1200 | return 0 1201 | positions['m2m'] = positions['m2m'].str.replace( 1202 | ',', '').astype(float) 1203 | return float(positions['m2m'].sum()) 1204 | 1205 | def _format_candles(self, data, interval): 1206 | records = data['data']['candles'] 1207 | df = pd.DataFrame(records, columns=[ 1208 | 'datetime', 'open', 'high', 'low', 'close', 'volume']) # , index=0) 1209 | df['datetime'] = df['datetime'].apply( 1210 | pd.Timestamp, unit='s', tzinfo=pytz.timezone('Asia/Kolkata')) 1211 | 1212 | df[['open', 'high', 'low', 'close']] = df[[ 1213 | 'open', 'high', 'low', 'close']].astype(float) 1214 | df['volume'] = df['volume'].astype(int) 1215 | df.set_index('datetime', inplace=True) 1216 | if interval in ['2H', '3H', '4H', 'W', 'M']: 1217 | if interval == 'W': 1218 | interval = 'W-Mon' 1219 | df = df.resample(interval, origin='start').agg({'open': 'first', 'high': 'max', 1220 | 'low': 'min', 'close': 'last', 'volume': 'sum'}).dropna() 1221 | df.index = df.index.astype(str).str[:-6] 1222 | return df 1223 | 1224 | def history(self, instrument, start_time=datetime.today() - timedelta(days=2), end_time=datetime.now(), interval=1, is_index=False): 1225 | exchange = instrument.exchange 1226 | start_time = int(start_time.timestamp()) 1227 | end_time = int(end_time.timestamp()) 1228 | if is_index: 1229 | exchange = 'NSE_INDICES' 1230 | 1231 | candle_type = self._candle_type[interval] 1232 | 1233 | data_duration = self._data_duration[interval] 1234 | 1235 | PARAMS = { 1236 | 'exchange': exchange, 1237 | 'token': instrument.token, 1238 | 'name': instrument.symbol, 1239 | 'candletype': candle_type, 1240 | 'starttime': start_time, 1241 | 'endtime': end_time, 1242 | 'type': 'all' 1243 | } 1244 | if data_duration is not None: 1245 | PARAMS['data_duration'] = data_duration 1246 | 1247 | headers = { 1248 | 'Content-Type': 'application/json', 1249 | 'Connection': 'Keep-Alive', 1250 | 'Accept': 'application/json' 1251 | } 1252 | r = requests.get( 1253 | 'https://alpha.sasonline.in/api/v1/charts/tdv', params=PARAMS, headers=headers) 1254 | data = r.json() 1255 | return self._format_candles(data, interval) 1256 | -------------------------------------------------------------------------------- /alphatrade/exceptions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | exceptions.py 4 | 5 | Exceptions raised by the Alpha Trade client. 6 | 7 | :license: see LICENSE for details. 8 | """ 9 | 10 | 11 | class AlphaException(Exception): 12 | """ 13 | Base exception class representing a Alpha Trade client exception. 14 | 15 | Every specific Alpha Trade client exception is a subclass of this 16 | and exposes two instance variables `.code` (HTTP error code) 17 | and `.message` (error text). 18 | """ 19 | 20 | def __init__(self, message, code=500): 21 | """Initialize the exception.""" 22 | super(AlphaException, self).__init__(message) 23 | self.code = code 24 | 25 | 26 | class GeneralException(AlphaException): 27 | """An unclassified, general error. Default code is 500.""" 28 | 29 | def __init__(self, message, code=500): 30 | """Initialize the exception.""" 31 | super(GeneralException, self).__init__(message, code) 32 | 33 | 34 | class TokenException(AlphaException): 35 | """Represents all token and authentication related errors. Default code is 403.""" 36 | 37 | def __init__(self, message, code=403): 38 | """Initialize the exception.""" 39 | super(TokenException, self).__init__(message, code) 40 | 41 | 42 | class PermissionException(AlphaException): 43 | """Represents permission denied exceptions for certain calls. Default code is 403.""" 44 | 45 | def __init__(self, message, code=403): 46 | """Initialize the exception.""" 47 | super(PermissionException, self).__init__(message, code) 48 | 49 | 50 | class OrderException(AlphaException): 51 | """Represents all order placement and manipulation errors. Default code is 500.""" 52 | 53 | def __init__(self, message, code=500): 54 | """Initialize the exception.""" 55 | super(OrderException, self).__init__(message, code) 56 | 57 | 58 | class InputException(AlphaException): 59 | """Represents user input errors such as missing and invalid parameters. Default code is 400.""" 60 | 61 | def __init__(self, message, code=400): 62 | """Initialize the exception.""" 63 | super(InputException, self).__init__(message, code) 64 | 65 | 66 | class DataException(AlphaException): 67 | """Represents a bad response from the backend Order Management System (OMS). Default code is 502.""" 68 | 69 | def __init__(self, message, code=502): 70 | """Initialize the exception.""" 71 | super(DataException, self).__init__(message, code) 72 | 73 | 74 | class NetworkException(AlphaException): 75 | """Represents a network issue between Alpha Trade and the backend Order Management System (OMS). Default code is 503.""" 76 | 77 | def __init__(self, message, code=503): 78 | """Initialize the exception.""" 79 | super(NetworkException, self).__init__(message, code) 80 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | setuptools==70.0.0 2 | schedule==1.2.0 3 | mock==5.1.0 4 | pytest==7.4.0 5 | twine==4.0.2 6 | wheel==0.40.0 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.32.2 2 | websocket-client==1.6.1 3 | protlib==1.5.0 4 | pandas==2.0.3 5 | pyotp==2.8.0 6 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | 4 | [bdist_wheel] 5 | universal=1 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open('README.md', 'r') as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name='alphatrade', 8 | packages=setuptools.find_packages(), 9 | version='1.0.0', 10 | include_package_data=True, 11 | description='Python APIs for SAS Online Alpha Trade Web Platform', 12 | long_description=long_description, 13 | long_description_content_type='text/markdown', author='Algo 2 Trade', 14 | author_email='help@algo2.trade', 15 | url='https://github.com/algo2t/alphatrade', 16 | install_requires=['setuptools==70.0.0','requests', 'websocket_client', 'protlib', 'pandas','pyotp'], 17 | keywords=['alphatrade', 'alpha-trade', 'sasonline', 18 | 'python', 'sdk', 'trading', 'stock markets'], 19 | python_requires='>=3.7', 20 | classifiers=[ 21 | 'Development Status :: 5 - Production/Stable', 22 | 'Intended Audience :: Developers', 23 | 'Intended Audience :: Financial and Insurance Industry', 24 | 'Natural Language :: English', 25 | 'Operating System :: OS Independent', 26 | 'License :: OSI Approved :: MIT License', 27 | 'Programming Language :: Python :: 3.8', 28 | 'Programming Language :: Python :: 3.9', 29 | 'Programming Language :: Python :: 3.10', 30 | 'Programming Language :: Python :: 3.11', 31 | 'Programming Language :: Python :: Implementation :: PyPy', 32 | 'Topic :: Software Development :: Libraries :: Python Modules', 33 | 'Topic :: Software Development :: Libraries' 34 | ], 35 | ) 36 | -------------------------------------------------------------------------------- /snaps/forget_password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algo2t/alphatrade/e1b3a4f946b8c438596bb34e109958eb83b41b90/snaps/forget_password.png -------------------------------------------------------------------------------- /snaps/reset_two_fa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algo2t/alphatrade/e1b3a4f946b8c438596bb34e109958eb83b41b90/snaps/reset_two_fa.png -------------------------------------------------------------------------------- /snaps/set_answers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/algo2t/alphatrade/e1b3a4f946b8c438596bb34e109958eb83b41b90/snaps/set_answers.png -------------------------------------------------------------------------------- /zexample_sas_login.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | from time import sleep 4 | 5 | # pip install https://github.com/algo2t/alphatrade 6 | 7 | from alphatrade import AlphaTrade, LiveFeedType 8 | 9 | import config 10 | # NOTE create a config.py file in the same directory as sas_login_eg.py file 11 | # Contents of the config.py must be as below, config.py is used for storing credentials 12 | #### config.py START #### 13 | # login_id = "RR" 14 | # password = "SAS@131" 15 | # TOTP = "EXAMPLETOTPSECRET" 16 | 17 | # try: 18 | # access_token = open('zaccess_token.txt', 'r').read().rstrip() 19 | # except Exception as e: 20 | # print('Exception occurred :: {}'.format(e)) 21 | # access_token = None 22 | #### config.py END #### 23 | 24 | sas = AlphaTrade(login_id=config.login_id, password=config.password, twofa=config.TOTP) 25 | 26 | # NOTE access_token can be supplied if already available 27 | # sas = AlphaTrade(login_id=config.login_id, password=config.password, 28 | # twofa=config.twofa, access_token=config.access_token) 29 | 30 | # NOTE access_token can be supplied if already available and master_contracts to download 31 | # sas = AlphaTrade(login_id=config.login_id, password=config.password, 32 | # twofa=config.twofa, access_token=config.access_token, master_contracts_to_download=['CDS']) 33 | 34 | 35 | print(sas.get_profile()) 36 | usd_inr = sas.get_instrument_by_symbol('NSE', 'PAYTM') 37 | print(usd_inr) 38 | print(sas.get_balance()) 39 | -------------------------------------------------------------------------------- /zhistorical_data.py: -------------------------------------------------------------------------------- 1 | import json 2 | from time import sleep 3 | from datetime import datetime, timedelta 4 | 5 | # pip install https://github.com/algo2t/alphatrade 6 | 7 | from alphatrade import AlphaTrade 8 | 9 | import config as config 10 | 11 | sas = AlphaTrade(login_id=config.login_id, 12 | password=config.password, twofa=config.TOTP) 13 | 14 | usd_inr = sas.get_instrument_by_symbol('CDS', 'USDINR SEP FUT') 15 | print(usd_inr) 16 | # print(sas.get_balance()) 17 | start_time = datetime(2022, 1, 9, 9, 15, 0) 18 | end_time = datetime.now() 19 | 20 | # df = sas.get_historical_candles( 21 | # 'MCX', 'NATURALGAS MAY FUT', start_time, end_time, 5) 22 | # print(df) 23 | # end_time = start_time + timedelta(days=5) 24 | # df = sas.get_historical_candles( 25 | # 'MCX', 'NATURALGAS APR FUT', start_time, end_time, 15) 26 | # print(df) 27 | 28 | # # Get Intraday Candles data based on interval - default 5 minute 29 | # df = sas.get_intraday_candles('MCX', 'NATURALGAS MAY FUT') 30 | # print(df) 31 | df = sas.get_intraday_candles('MCX', 'NATURALGAS AUG FUT', 15) 32 | print(df) 33 | 34 | # # Get Historical candles data 35 | # print(sas.get_historical_candles('MCX', 'NATURALGAS APR FUT', 36 | # datetime(2020, 10, 19), datetime.now(), interval=30)) 37 | 38 | # # Get Historical candle for Nifty Bank and India VIX 39 | # india_vix_nse_index = sas.get_instrument_by_symbol('NSE', 'India VIX') 40 | # print(sas.get_historical_candles(india_vix_nse_index.exchange, 41 | # india_vix_nse_index.symbol, datetime(2020, 11, 30), datetime.now(), interval=30, is_index=True)) 42 | 43 | nifty_bank_nse_index = sas.get_instrument_by_symbol('NSE', 'Nifty Bank') 44 | # print(nifty_bank_nse_index) 45 | print(sas.history(nifty_bank_nse_index, datetime(2022, 2, 2, 9, 15, 0), datetime.now(), interval=30, is_index=True)) 46 | 47 | -------------------------------------------------------------------------------- /zlogin_example.py: -------------------------------------------------------------------------------- 1 | 2 | from alphatrade import AlphaTrade 3 | import config 4 | import pyotp 5 | Totp = config.TOTP 6 | pin = pyotp.TOTP(Totp).now() 7 | totp = f"{int(pin):06d}" if len(pin) <= 5 else pin 8 | print(totp) 9 | sas = AlphaTrade(login_id=config.login_id, password=config.password, 10 | twofa=totp, access_token=config.access_token, master_contracts_to_download=['MCX', 'NFO']) 11 | # print(sas.get_profile()) 12 | print(sas.get_balance()) 13 | print(sas.get_trade_book()) 14 | # print(sas.orders('complete')) 15 | # print(sas.orders('pending')) 16 | print(sas.orders()) 17 | # print(sas.get_daywise_positions()) 18 | # print(sas.get_holding_positions()) 19 | # print(sas.get_netwise_positions()) 20 | print(sas.search('Nifty Bank','NSE')) 21 | print(sas.search('TCS','BSE')) 22 | print(sas.search('TCS', 'NFO')) 23 | print(sas.search('TCS-EQ')) 24 | print(sas.positions()) 25 | print(sas.positions('historical')) 26 | 27 | print(sas.get_exchanges()) 28 | print(sas.get_master_contract('MCX')) 29 | 30 | -------------------------------------------------------------------------------- /zstop.txt: -------------------------------------------------------------------------------- 1 | stop 2 | -------------------------------------------------------------------------------- /zstreaming_data.py: -------------------------------------------------------------------------------- 1 | import statistics 2 | import datetime 3 | import config 4 | import json 5 | from time import sleep 6 | 7 | # pip install https://github.com/algo2t/alphatrade 8 | 9 | from alphatrade import AlphaTrade, LiveFeedType 10 | 11 | sas = AlphaTrade(login_id=config.login_id, password=config.password, 12 | twofa=config.TOTP, access_token=config.access_token, master_contracts_to_download=['MCX', 'NFO']) 13 | 14 | 15 | ins_scrip = sas.get_instrument_by_symbol('MCX', 'NATURALGAS AUG FUT') 16 | 17 | print(ins_scrip) 18 | 19 | ltp = 0.0 20 | socket_opened = False 21 | count_s = 0 22 | count_b = 0 23 | 24 | 25 | def event_handler_quote_update(message): 26 | global ltp 27 | # ltp = message['ltp'] 28 | # tick = json.loads(message, indent=1) 29 | print(f'ticks :: {message}') 30 | 31 | 32 | def open_callback(): 33 | global socket_opened 34 | socket_opened = True 35 | 36 | 37 | def run_strategy(): 38 | global ltp 39 | minute_close = [] 40 | 41 | sas.start_websocket(subscribe_callback=event_handler_quote_update, 42 | socket_open_callback=open_callback, 43 | run_in_background=True) 44 | while (socket_opened is False): # wait till socket open & then subscribe 45 | pass 46 | sas.subscribe(ins_scrip, LiveFeedType.COMPACT) 47 | # sas.subscribe(ins_scrip, LiveFeedType.MARKET_DATA) 48 | # sas.subscribe(ins_scrip, LiveFeedType.SNAPQUOTE) 49 | # sas.subscribe(ins_scrip, LiveFeedType.FULL_SNAPQUOTE) 50 | 51 | print("Script Start Time :: " + str(datetime.datetime.now())) 52 | while True: 53 | print('current price is :: '+str(ltp)) 54 | if datetime.datetime.now().second in [0, 30]: 55 | # NOTE This is just an example to stop script without using `control + c` Keyboard Interrupt 56 | # It checks whether the stop.txt has word stop 57 | # This check is done every 30 seconds 58 | stop_script = open('zstop.txt', 'r').read().strip() 59 | print(stop_script + " time :: " + str(datetime.datetime.now())) 60 | if (stop_script == 'stop'): 61 | print('exiting script') 62 | break 63 | # LTP is stored in the minute_close array and then mean is found on close to get SMA_5 and SMA_20 64 | if (datetime.datetime.now().second == 0): 65 | minute_close.append(ltp) 66 | print('current price is :: '+str(ltp)) 67 | if (len(minute_close) > 20): 68 | sma_5 = statistics.mean(minute_close[-5:]) 69 | sma_20 = statistics.mean(minute_close[-20:]) 70 | print('current price is :: '+str(ltp)) 71 | if (sma_5 > sma_20) and (status != 'bought'): 72 | # buy_signal(ins_scrip) 73 | print('buy signal crossover :: ' + str(count_b)) 74 | status = 'bought' 75 | count_b = count_b+1 76 | elif (sma_5 < sma_20) and (status != 'sold'): 77 | # sell_signal(ins_scrip) 78 | print('sell signal crossover :: ' + str(counts)) 79 | status = 'sold' 80 | counts = counts + 1 81 | sleep(1) 82 | sleep(0.2) # sleep for 200ms 83 | 84 | 85 | run_strategy() 86 | --------------------------------------------------------------------------------