vortex-api 1.0.5__tar.gz → 1.0.6.dev2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {vortex_api-1.0.5 → vortex_api-1.0.6.dev2}/PKG-INFO +27 -1
- {vortex_api-1.0.5 → vortex_api-1.0.6.dev2}/README.md +26 -0
- {vortex_api-1.0.5 → vortex_api-1.0.6.dev2}/setup.py +5 -1
- {vortex_api-1.0.5 → vortex_api-1.0.6.dev2}/vortex_api/__init__.py +2 -1
- {vortex_api-1.0.5 → vortex_api-1.0.6.dev2}/vortex_api/__version__.py +1 -1
- {vortex_api-1.0.5 → vortex_api-1.0.6.dev2}/vortex_api/api.py +0 -5
- vortex_api-1.0.6.dev2/vortex_api/vortex_feed.py +604 -0
- {vortex_api-1.0.5 → vortex_api-1.0.6.dev2}/vortex_api.egg-info/PKG-INFO +27 -1
- {vortex_api-1.0.5 → vortex_api-1.0.6.dev2}/vortex_api.egg-info/SOURCES.txt +1 -0
- vortex_api-1.0.6.dev2/vortex_api.egg-info/requires.txt +6 -0
- vortex_api-1.0.5/vortex_api.egg-info/requires.txt +0 -2
- {vortex_api-1.0.5 → vortex_api-1.0.6.dev2}/LICENSE +0 -0
- {vortex_api-1.0.5 → vortex_api-1.0.6.dev2}/setup.cfg +0 -0
- {vortex_api-1.0.5 → vortex_api-1.0.6.dev2}/vortex_api.egg-info/dependency_links.txt +0 -0
- {vortex_api-1.0.5 → vortex_api-1.0.6.dev2}/vortex_api.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: vortex_api
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.6.dev2
|
|
4
4
|
Summary: Vortex APIs to place orders in AsthaTrade Flow application
|
|
5
5
|
Home-page: https://vortex.asthatrade.com
|
|
6
6
|
Download-URL: https://github.com/AsthaTech/pyvortex
|
|
@@ -57,6 +57,32 @@ client.place_order(
|
|
|
57
57
|
client.orders(limit=20,offset=1)
|
|
58
58
|
|
|
59
59
|
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
# Connecting to websocket
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
from vortex_api import VortexFeed
|
|
66
|
+
from vortex_api import Constants as Vc
|
|
67
|
+
|
|
68
|
+
def main():
|
|
69
|
+
# Get access token from any of the login methods
|
|
70
|
+
wire = VortexFeed(access_token)
|
|
71
|
+
|
|
72
|
+
wire.on_price_update = on_price_update
|
|
73
|
+
wire.on_order_update = on_order_update
|
|
74
|
+
wire.on_connect = on_connect
|
|
75
|
+
|
|
76
|
+
def on_price_update(ws,data):
|
|
77
|
+
print(data)
|
|
78
|
+
|
|
79
|
+
def on_order_update(ws,data):
|
|
80
|
+
print(data)
|
|
81
|
+
|
|
82
|
+
def on_connect(ws, response):
|
|
83
|
+
ws.subscribe(Vc.ExchangeTypes.NSE_EQUITY, 26000) # Subscribe to NIFTY 50
|
|
84
|
+
ws.subscribe(Vc.ExchangeTypes.NSE_EQUITY, 26000) # Subscribe to BANKNIFTY
|
|
85
|
+
|
|
60
86
|
```
|
|
61
87
|
Refer to the [python document](https://vortex.asthatrade.com/docs/pyvortex/vortex_api.html) for all methods and features
|
|
62
88
|
|
|
@@ -36,6 +36,32 @@ client.place_order(
|
|
|
36
36
|
client.orders(limit=20,offset=1)
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
# Connecting to websocket
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
from vortex_api import VortexFeed
|
|
45
|
+
from vortex_api import Constants as Vc
|
|
46
|
+
|
|
47
|
+
def main():
|
|
48
|
+
# Get access token from any of the login methods
|
|
49
|
+
wire = VortexFeed(access_token)
|
|
50
|
+
|
|
51
|
+
wire.on_price_update = on_price_update
|
|
52
|
+
wire.on_order_update = on_order_update
|
|
53
|
+
wire.on_connect = on_connect
|
|
54
|
+
|
|
55
|
+
def on_price_update(ws,data):
|
|
56
|
+
print(data)
|
|
57
|
+
|
|
58
|
+
def on_order_update(ws,data):
|
|
59
|
+
print(data)
|
|
60
|
+
|
|
61
|
+
def on_connect(ws, response):
|
|
62
|
+
ws.subscribe(Vc.ExchangeTypes.NSE_EQUITY, 26000) # Subscribe to NIFTY 50
|
|
63
|
+
ws.subscribe(Vc.ExchangeTypes.NSE_EQUITY, 26000) # Subscribe to BANKNIFTY
|
|
64
|
+
|
|
39
65
|
```
|
|
40
66
|
Refer to the [python document](https://vortex.asthatrade.com/docs/pyvortex/vortex_api.html) for all methods and features
|
|
41
67
|
|
|
@@ -32,7 +32,11 @@ setup(
|
|
|
32
32
|
packages=["vortex_api"],
|
|
33
33
|
install_requires=[
|
|
34
34
|
"requests>=2.25.1",
|
|
35
|
-
"wrapt>=1.15.0"
|
|
35
|
+
"wrapt>=1.15.0",
|
|
36
|
+
"six>=1.11.0",
|
|
37
|
+
"pyOpenSSL>=17.5.0",
|
|
38
|
+
"python-dateutil>=2.6.1",
|
|
39
|
+
"autobahn[twisted]==19.11.2"
|
|
36
40
|
],
|
|
37
41
|
classifiers=[
|
|
38
42
|
"Intended Audience :: Developers",
|
|
@@ -34,4 +34,5 @@ Getting started
|
|
|
34
34
|
"""
|
|
35
35
|
from __future__ import unicode_literals, absolute_import
|
|
36
36
|
from vortex_api.api import AsthaTradeVortexAPI,Constants
|
|
37
|
-
|
|
37
|
+
from vortex_api.vortex_feed import VortexFeed
|
|
38
|
+
__all__ = [AsthaTradeVortexAPI,Constants,VortexFeed]
|
|
@@ -2,7 +2,7 @@ __name__ = "vortex_api"
|
|
|
2
2
|
__description__ = "Vortex APIs to place orders in AsthaTrade Flow application"
|
|
3
3
|
__url__ = "https://vortex.asthatrade.com"
|
|
4
4
|
__download_url__ = "https://github.com/AsthaTech/pyvortex"
|
|
5
|
-
__version__ = "1.0.
|
|
5
|
+
__version__ = "1.0.6.dev2"
|
|
6
6
|
__author__ = "Astha Credit & Securities Pvt Ltd."
|
|
7
7
|
__author_email__ = "tech@asthatrade.com"
|
|
8
8
|
__license__ = "MIT"
|
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
import six
|
|
2
|
+
import sys
|
|
3
|
+
import time
|
|
4
|
+
import json
|
|
5
|
+
import struct
|
|
6
|
+
import logging
|
|
7
|
+
import threading
|
|
8
|
+
from twisted.internet import reactor, ssl
|
|
9
|
+
from twisted.python import log as twisted_log
|
|
10
|
+
from twisted.internet.protocol import ReconnectingClientFactory
|
|
11
|
+
from autobahn.twisted.websocket import WebSocketClientProtocol, \
|
|
12
|
+
WebSocketClientFactory, connectWS
|
|
13
|
+
|
|
14
|
+
from .__version__ import __version__, __name__
|
|
15
|
+
|
|
16
|
+
log = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
class ClientProtocol(WebSocketClientProtocol):
|
|
19
|
+
"""
|
|
20
|
+
A WebSocket client protocol that implements ping-pong keepalive.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
PING_INTERVAL: The interval in seconds between sending pings.
|
|
24
|
+
KEEPALIVE_INTERVAL: The interval in seconds after which a connection is considered dead if no pongs have been received.
|
|
25
|
+
"""
|
|
26
|
+
PING_INTERVAL = 2.5
|
|
27
|
+
KEEPALIVE_INTERVAL = 5
|
|
28
|
+
|
|
29
|
+
_ping_message = ""
|
|
30
|
+
_next_ping = None
|
|
31
|
+
_next_pong_check = None
|
|
32
|
+
_last_pong_time = None
|
|
33
|
+
_last_ping_time = None
|
|
34
|
+
|
|
35
|
+
def __init__(self, *args, **kwargs):
|
|
36
|
+
super(ClientProtocol, self).__init__(*args, **kwargs)
|
|
37
|
+
|
|
38
|
+
def onConnect(self, response):
|
|
39
|
+
"""
|
|
40
|
+
Called when the connection is established.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
response: The response from the server.
|
|
44
|
+
"""
|
|
45
|
+
self.factory.ws = self
|
|
46
|
+
|
|
47
|
+
if self.factory.on_connect:
|
|
48
|
+
self.factory.on_connect(self, response)
|
|
49
|
+
|
|
50
|
+
# Reset reconnect on successful reconnect
|
|
51
|
+
self.factory.resetDelay()
|
|
52
|
+
|
|
53
|
+
def onOpen(self):
|
|
54
|
+
"""
|
|
55
|
+
Called when the connection is opened.
|
|
56
|
+
|
|
57
|
+
Sends a ping and starts a timer to check for pongs.
|
|
58
|
+
"""
|
|
59
|
+
# send ping
|
|
60
|
+
self._loop_ping()
|
|
61
|
+
# Start a timer to check for pongs
|
|
62
|
+
self._loop_pong_check()
|
|
63
|
+
|
|
64
|
+
if self.factory.on_open:
|
|
65
|
+
self.factory.on_open(self)
|
|
66
|
+
|
|
67
|
+
def onMessage(self, payload, is_binary):
|
|
68
|
+
"""
|
|
69
|
+
Called when a message is received.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
payload: The message payload.
|
|
73
|
+
is_binary: Whether the message is binary.
|
|
74
|
+
"""
|
|
75
|
+
if self.factory.on_message:
|
|
76
|
+
self.factory.on_message(self, payload, is_binary)
|
|
77
|
+
|
|
78
|
+
def onClose(self, was_clean, code, reason):
|
|
79
|
+
"""
|
|
80
|
+
Called when the connection is closed.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
was_clean: Whether the connection was closed cleanly.
|
|
84
|
+
code: The close code.
|
|
85
|
+
reason: The close reason.
|
|
86
|
+
"""
|
|
87
|
+
print("was_clean", was_clean)
|
|
88
|
+
if not was_clean:
|
|
89
|
+
if self.factory.on_error:
|
|
90
|
+
self.factory.on_error(self, code, reason)
|
|
91
|
+
|
|
92
|
+
if self.factory.on_close:
|
|
93
|
+
self.factory.on_close(self, code, reason)
|
|
94
|
+
|
|
95
|
+
# Cancel next ping and timer
|
|
96
|
+
self._last_ping_time = None
|
|
97
|
+
self._last_pong_time = None
|
|
98
|
+
|
|
99
|
+
if self._next_ping:
|
|
100
|
+
self._next_ping.cancel()
|
|
101
|
+
|
|
102
|
+
if self._next_pong_check:
|
|
103
|
+
self._next_pong_check.cancel()
|
|
104
|
+
|
|
105
|
+
def onPong(self, response):
|
|
106
|
+
"""
|
|
107
|
+
Called when a pong message is received.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
response: The pong message.
|
|
111
|
+
"""
|
|
112
|
+
if self._last_pong_time and self.factory.debug:
|
|
113
|
+
log.debug("last pong was {} seconds back.".format(time.time() - self._last_pong_time))
|
|
114
|
+
|
|
115
|
+
self._last_pong_time = time.time()
|
|
116
|
+
|
|
117
|
+
if self.factory.debug:
|
|
118
|
+
log.debug("pong => {}".format(response))
|
|
119
|
+
|
|
120
|
+
def _loop_ping(self):
|
|
121
|
+
"""
|
|
122
|
+
Sends a ping message every X seconds.
|
|
123
|
+
"""
|
|
124
|
+
if self.factory.debug:
|
|
125
|
+
log.debug("ping => {}".format(self._ping_message))
|
|
126
|
+
if self._last_ping_time:
|
|
127
|
+
log.debug("last ping was {} seconds back.".format(time.time() - self._last_ping_time))
|
|
128
|
+
|
|
129
|
+
# Set current time as last ping time
|
|
130
|
+
self._last_ping_time = time.time()
|
|
131
|
+
# Send a ping message to server
|
|
132
|
+
self.sendPing(self._ping_message)
|
|
133
|
+
|
|
134
|
+
# Call self after X seconds
|
|
135
|
+
self._next_ping = self.factory.reactor.callLater(self.PING_INTERVAL, self._loop_ping)
|
|
136
|
+
|
|
137
|
+
def _loop_pong_check(self):
|
|
138
|
+
"""
|
|
139
|
+
Checks if the connection is still alive by checking the last pong time.
|
|
140
|
+
If no pong has been received in X seconds, the connection is considered dead and is dropped.
|
|
141
|
+
"""
|
|
142
|
+
if self._last_pong_time:
|
|
143
|
+
# No pong message since long time, so init reconnect
|
|
144
|
+
last_pong_diff = time.time() - self._last_pong_time
|
|
145
|
+
if last_pong_diff > (2 * self.PING_INTERVAL):
|
|
146
|
+
if self.factory.debug:
|
|
147
|
+
log.debug("Last pong was {} seconds ago. So dropping connection to reconnect.".format(
|
|
148
|
+
last_pong_diff))
|
|
149
|
+
# drop existing connection to avoid ghost connection
|
|
150
|
+
self.dropConnection(abort=True)
|
|
151
|
+
|
|
152
|
+
# Call self after X seconds
|
|
153
|
+
self._next_pong_check = self.factory.reactor.callLater(self.PING_INTERVAL, self._loop_pong_check)
|
|
154
|
+
|
|
155
|
+
class ClientFactory(WebSocketClientFactory,ReconnectingClientFactory):
|
|
156
|
+
"""
|
|
157
|
+
A WebSocket client factory that implements reconnect logic.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
protocol: The WebSocket protocol to use.
|
|
161
|
+
maxDelay: The maximum delay in seconds between retries.
|
|
162
|
+
maxRetries: The maximum number of retries.
|
|
163
|
+
"""
|
|
164
|
+
protocol = ClientProtocol
|
|
165
|
+
maxDelay = 5
|
|
166
|
+
maxRetries = 10
|
|
167
|
+
|
|
168
|
+
_last_connection_time = None
|
|
169
|
+
|
|
170
|
+
def __init__(self, *args, **kwargs):
|
|
171
|
+
"""Initialize with default callback method values."""
|
|
172
|
+
self.debug = True
|
|
173
|
+
self.ws = None
|
|
174
|
+
self.on_open = None
|
|
175
|
+
self.on_error = None
|
|
176
|
+
self.on_close = None
|
|
177
|
+
self.on_message = None
|
|
178
|
+
self.on_connect = None
|
|
179
|
+
self.on_reconnect = None
|
|
180
|
+
self.on_noreconnect = None
|
|
181
|
+
|
|
182
|
+
super(ClientFactory, self).__init__(*args, **kwargs)
|
|
183
|
+
|
|
184
|
+
def startedConnecting(self, connector):
|
|
185
|
+
"""Called when the connection is started or reconnected."""
|
|
186
|
+
if not self._last_connection_time and self.debug:
|
|
187
|
+
log.debug("Start WebSocket connection.")
|
|
188
|
+
|
|
189
|
+
self._last_connection_time = time.time()
|
|
190
|
+
|
|
191
|
+
def clientConnectionFailed(self, connector, reason):
|
|
192
|
+
"""Called when the connection fails."""
|
|
193
|
+
if self.retries > 0:
|
|
194
|
+
log.error("Retrying connection. Retry attempt count: {}. Next retry in around: {} seconds".format(self.retries, int(round(self.delay))))
|
|
195
|
+
|
|
196
|
+
# on reconnect callback
|
|
197
|
+
if self.on_reconnect:
|
|
198
|
+
self.on_reconnect(self.retries)
|
|
199
|
+
|
|
200
|
+
# Retry the connection
|
|
201
|
+
self.retry(connector)
|
|
202
|
+
self.send_noreconnect()
|
|
203
|
+
|
|
204
|
+
def clientConnectionLost(self, connector, reason):
|
|
205
|
+
"""Called when the connection is lost."""
|
|
206
|
+
if self.retries > 0:
|
|
207
|
+
# on reconnect callback
|
|
208
|
+
if self.on_reconnect:
|
|
209
|
+
self.on_reconnect(self.retries)
|
|
210
|
+
|
|
211
|
+
# Retry the connection
|
|
212
|
+
self.retry(connector)
|
|
213
|
+
self.send_noreconnect()
|
|
214
|
+
|
|
215
|
+
def send_noreconnect(self):
|
|
216
|
+
"""Called when the maximum number of retries has been exhausted."""
|
|
217
|
+
if self.maxRetries is not None and (self.retries > self.maxRetries):
|
|
218
|
+
if self.debug:
|
|
219
|
+
log.debug("Maximum retries ({}) exhausted.".format(self.maxRetries))
|
|
220
|
+
# Stop the loop for exceeding max retry attempts
|
|
221
|
+
self.stop()
|
|
222
|
+
|
|
223
|
+
if self.on_noreconnect:
|
|
224
|
+
self.on_noreconnect()
|
|
225
|
+
|
|
226
|
+
class VortexFeed:
|
|
227
|
+
"""
|
|
228
|
+
The WebSocket client for connecting to vortex's live price and order streaming service
|
|
229
|
+
"""
|
|
230
|
+
CONNECT_TIMEOUT = 30
|
|
231
|
+
# Default Reconnect max delay.
|
|
232
|
+
RECONNECT_MAX_DELAY = 60
|
|
233
|
+
# Default reconnect attempts
|
|
234
|
+
RECONNECT_MAX_TRIES = 50
|
|
235
|
+
_is_first_connect = True
|
|
236
|
+
_message_subscribe = "subscribe"
|
|
237
|
+
_message_unsubscribe = "unsubscribe"
|
|
238
|
+
|
|
239
|
+
def __init__(self, access_token: str, websocket_endpoint="wss://wire.asthatrade.com/ws",reconnect=True, reconnect_max_tries=RECONNECT_MAX_TRIES, reconnect_max_delay=RECONNECT_MAX_DELAY,
|
|
240
|
+
connect_timeout=CONNECT_TIMEOUT) -> None:
|
|
241
|
+
self._maximum_reconnect_max_tries = self.RECONNECT_MAX_TRIES
|
|
242
|
+
self._minimum_reconnect_max_delay = 0
|
|
243
|
+
if reconnect == False:
|
|
244
|
+
self.reconnect_max_tries = 0
|
|
245
|
+
elif reconnect_max_tries > self._maximum_reconnect_max_tries:
|
|
246
|
+
log.warning("`reconnect_max_tries` can not be more than {val}. Setting to highest possible value - {val}.".format(
|
|
247
|
+
val=self._maximum_reconnect_max_tries))
|
|
248
|
+
self.reconnect_max_tries = self._maximum_reconnect_max_tries
|
|
249
|
+
else:
|
|
250
|
+
self.reconnect_max_tries = reconnect_max_tries
|
|
251
|
+
|
|
252
|
+
if reconnect_max_delay < self._minimum_reconnect_max_delay:
|
|
253
|
+
log.warning("`reconnect_max_delay` can not be less than {val}. Setting to lowest possible value - {val}.".format(
|
|
254
|
+
val=self._minimum_reconnect_max_delay))
|
|
255
|
+
self.reconnect_max_delay = self._minimum_reconnect_max_delay
|
|
256
|
+
else:
|
|
257
|
+
self.reconnect_max_delay = reconnect_max_delay
|
|
258
|
+
|
|
259
|
+
self.connect_timeout = connect_timeout
|
|
260
|
+
self.socket_url = websocket_endpoint+"?auth_token="+access_token
|
|
261
|
+
self.access_token = access_token
|
|
262
|
+
self.socket_token = self.__getSocketToken__(self.access_token)
|
|
263
|
+
|
|
264
|
+
self.debug = True
|
|
265
|
+
# self.on_price_update = None
|
|
266
|
+
self.on_price_update = None
|
|
267
|
+
self.on_open = None
|
|
268
|
+
self.on_close = None
|
|
269
|
+
self.on_error = None
|
|
270
|
+
self.on_connect = None
|
|
271
|
+
self.on_message = None
|
|
272
|
+
self.on_reconnect = None
|
|
273
|
+
self.on_noreconnect = None
|
|
274
|
+
self.on_order_update = None
|
|
275
|
+
self.subscribed_tokens = {}
|
|
276
|
+
pass
|
|
277
|
+
|
|
278
|
+
def __getSocketToken__(self,access_token: str)->str:
|
|
279
|
+
return
|
|
280
|
+
|
|
281
|
+
def _create_connection(self, url, **kwargs):
|
|
282
|
+
self.factory = ClientFactory(url, **kwargs)
|
|
283
|
+
self.ws = self.factory.ws
|
|
284
|
+
self.factory.debug = self.debug
|
|
285
|
+
|
|
286
|
+
self.factory.on_open = self._on_open
|
|
287
|
+
self.factory.on_error = self._on_error
|
|
288
|
+
self.factory.on_close = self._on_close
|
|
289
|
+
self.factory.on_message = self._on_message
|
|
290
|
+
self.factory.on_connect = self._on_connect
|
|
291
|
+
self.factory.on_reconnect = self._on_reconnect
|
|
292
|
+
self.factory.on_noreconnect = self._on_noreconnect
|
|
293
|
+
|
|
294
|
+
self.factory.maxDelay = self.reconnect_max_delay
|
|
295
|
+
self.factory.maxRetries = self.reconnect_max_tries
|
|
296
|
+
|
|
297
|
+
def _user_agent(self):
|
|
298
|
+
return (__name__ + "-python/").capitalize() + __version__
|
|
299
|
+
|
|
300
|
+
def connect(self, threaded=False, disable_ssl_verification=False):
|
|
301
|
+
"""
|
|
302
|
+
Establish a websocket connection.
|
|
303
|
+
- `disable_ssl_verification` disables building ssl context
|
|
304
|
+
"""
|
|
305
|
+
# Init WebSocket client factory
|
|
306
|
+
self._create_connection(self.socket_url,
|
|
307
|
+
useragent=self._user_agent())
|
|
308
|
+
|
|
309
|
+
# Set SSL context
|
|
310
|
+
context_factory = None
|
|
311
|
+
if self.factory.isSecure and not disable_ssl_verification:
|
|
312
|
+
context_factory = ssl.ClientContextFactory()
|
|
313
|
+
|
|
314
|
+
# Establish WebSocket connection to a server
|
|
315
|
+
connectWS(self.factory, contextFactory=context_factory, timeout=self.connect_timeout)
|
|
316
|
+
|
|
317
|
+
if self.debug:
|
|
318
|
+
twisted_log.startLogging(sys.stdout)
|
|
319
|
+
|
|
320
|
+
# Run in seperate thread of blocking
|
|
321
|
+
opts = {}
|
|
322
|
+
# Run when reactor is not running
|
|
323
|
+
if not reactor.running:
|
|
324
|
+
if threaded:
|
|
325
|
+
# Signals are not allowed in non main thread by twisted so suppress it.
|
|
326
|
+
opts["installSignalHandlers"] = False
|
|
327
|
+
self.websocket_thread = threading.Thread(target=reactor.run, kwargs=opts)
|
|
328
|
+
self.websocket_thread.daemon = True
|
|
329
|
+
self.websocket_thread.start()
|
|
330
|
+
else:
|
|
331
|
+
reactor.run(**opts)
|
|
332
|
+
else:
|
|
333
|
+
print(reactor.running)
|
|
334
|
+
|
|
335
|
+
def is_connected(self):
|
|
336
|
+
"""Check if WebSocket connection is established."""
|
|
337
|
+
if self.ws and self.ws.state == self.ws.STATE_OPEN:
|
|
338
|
+
return True
|
|
339
|
+
else:
|
|
340
|
+
return False
|
|
341
|
+
|
|
342
|
+
def _close(self, code=None, reason=None):
|
|
343
|
+
"""Close the WebSocket connection."""
|
|
344
|
+
if self.ws:
|
|
345
|
+
self.ws.sendClose(code, reason)
|
|
346
|
+
|
|
347
|
+
def close(self, code=None, reason=None):
|
|
348
|
+
"""Close the WebSocket connection."""
|
|
349
|
+
self.stop_retry()
|
|
350
|
+
self._close(code, reason)
|
|
351
|
+
|
|
352
|
+
def stop(self):
|
|
353
|
+
"""Stop the event loop. Should be used if main thread has to be closed in `on_close` method.
|
|
354
|
+
Reconnection mechanism cannot happen past this method
|
|
355
|
+
"""
|
|
356
|
+
reactor.stop()
|
|
357
|
+
|
|
358
|
+
def stop_retry(self):
|
|
359
|
+
"""Stop auto retry when it is in progress."""
|
|
360
|
+
if self.factory:
|
|
361
|
+
self.factory.stopTrying()
|
|
362
|
+
|
|
363
|
+
def subscribe(self, exchange,token,mode):
|
|
364
|
+
"""
|
|
365
|
+
Subscribe to a list of instrument_tokens.
|
|
366
|
+
- `instrument_tokens` is list of instrument instrument_tokens to subscribe
|
|
367
|
+
"""
|
|
368
|
+
try:
|
|
369
|
+
self.ws.sendMessage(six.b(json.dumps({"message_type": self._message_subscribe, "segment_id": exchange,"token": token,"mode": mode})))
|
|
370
|
+
|
|
371
|
+
try:
|
|
372
|
+
self.subscribed_tokens[exchange][token] = mode
|
|
373
|
+
except KeyError:
|
|
374
|
+
self.subscribed_tokens[exchange] = {}
|
|
375
|
+
self.subscribed_tokens[exchange][token] = mode
|
|
376
|
+
|
|
377
|
+
return True
|
|
378
|
+
except Exception as e:
|
|
379
|
+
self._close(reason="Error while subscribe: {}".format(str(e)))
|
|
380
|
+
raise
|
|
381
|
+
|
|
382
|
+
def unsubscribe(self, exchange,token):
|
|
383
|
+
"""
|
|
384
|
+
Unsubscribe the given list of instrument_tokens.
|
|
385
|
+
- `instrument_tokens` is list of instrument_tokens to unsubscribe.
|
|
386
|
+
"""
|
|
387
|
+
try:
|
|
388
|
+
self.ws.sendMessage(six.b(json.dumps({"message_type": self._message_unsubscribe, "segment_id": exchange,"token": token})))
|
|
389
|
+
|
|
390
|
+
try:
|
|
391
|
+
del(self.subscribed_tokens[exchange][token])
|
|
392
|
+
except KeyError:
|
|
393
|
+
pass
|
|
394
|
+
|
|
395
|
+
return True
|
|
396
|
+
except Exception as e:
|
|
397
|
+
self._close(reason="Error while unsubscribe: {}".format(str(e)))
|
|
398
|
+
raise
|
|
399
|
+
|
|
400
|
+
def resubscribe(self):
|
|
401
|
+
"""Resubscribe to all current subscribed tokens."""
|
|
402
|
+
modes = {}
|
|
403
|
+
|
|
404
|
+
for exchange in self.subscribed_tokens:
|
|
405
|
+
for token in self.subscribed_tokens[exchange]:
|
|
406
|
+
self.subscribe(exchange=exchange, token=token)
|
|
407
|
+
|
|
408
|
+
for token in self.subscribed_tokens:
|
|
409
|
+
m = self.subscribed_tokens[token]
|
|
410
|
+
|
|
411
|
+
if not modes.get(m):
|
|
412
|
+
modes[m] = []
|
|
413
|
+
|
|
414
|
+
modes[m].append(token)
|
|
415
|
+
|
|
416
|
+
for mode in modes:
|
|
417
|
+
if self.debug:
|
|
418
|
+
log.debug("Resubscribe and set mode: {} - {}".format(mode, modes[mode]))
|
|
419
|
+
|
|
420
|
+
self.subscribe(modes[mode])
|
|
421
|
+
|
|
422
|
+
def _on_connect(self, ws, response):
|
|
423
|
+
self.ws = ws
|
|
424
|
+
if self.on_connect:
|
|
425
|
+
self.on_connect(self, response)
|
|
426
|
+
|
|
427
|
+
def _on_close(self, ws, code, reason):
|
|
428
|
+
"""Call `on_close` callback when connection is closed."""
|
|
429
|
+
log.error("Connection closed: {} - {}".format(code, str(reason)))
|
|
430
|
+
|
|
431
|
+
if self.on_close:
|
|
432
|
+
self.on_close(self, code, reason)
|
|
433
|
+
|
|
434
|
+
def _on_error(self, ws, code, reason):
|
|
435
|
+
"""Call `on_error` callback when connection throws an error."""
|
|
436
|
+
log.error("Connection error: {} - {}".format(code, str(reason)))
|
|
437
|
+
|
|
438
|
+
if self.on_error:
|
|
439
|
+
self.on_error(self, code, reason)
|
|
440
|
+
|
|
441
|
+
def _on_message(self, ws, payload, is_binary):
|
|
442
|
+
"""Call `on_message` callback when text message is received."""
|
|
443
|
+
if self.on_message:
|
|
444
|
+
self.on_message(self, payload, is_binary)
|
|
445
|
+
|
|
446
|
+
# If the message is binary, parse it and send it to the callback.
|
|
447
|
+
if self.on_price_update and is_binary and len(payload) > 4:
|
|
448
|
+
self.on_price_update(self, self._parse_binary(payload))
|
|
449
|
+
|
|
450
|
+
# Parse text messages
|
|
451
|
+
if not is_binary:
|
|
452
|
+
self._parse_text_message(payload)
|
|
453
|
+
|
|
454
|
+
def _on_open(self, ws):
|
|
455
|
+
# Resubscribe if its reconnect
|
|
456
|
+
if not self._is_first_connect:
|
|
457
|
+
self.resubscribe()
|
|
458
|
+
|
|
459
|
+
# Set first connect to false once its connected first time
|
|
460
|
+
self._is_first_connect = False
|
|
461
|
+
|
|
462
|
+
if self.on_open:
|
|
463
|
+
return self.on_open(self)
|
|
464
|
+
|
|
465
|
+
def _on_reconnect(self, attempts_count):
|
|
466
|
+
if self.on_reconnect:
|
|
467
|
+
return self.on_reconnect(self, attempts_count)
|
|
468
|
+
|
|
469
|
+
def _on_noreconnect(self):
|
|
470
|
+
if self.on_noreconnect:
|
|
471
|
+
return self.on_noreconnect(self)
|
|
472
|
+
|
|
473
|
+
def _parse_text_message(self, payload):
|
|
474
|
+
"""Parse text message."""
|
|
475
|
+
# Decode unicode data
|
|
476
|
+
if not six.PY2 and type(payload) == bytes:
|
|
477
|
+
payload = payload.decode("utf-8")
|
|
478
|
+
|
|
479
|
+
try:
|
|
480
|
+
data = json.loads(payload)
|
|
481
|
+
except ValueError:
|
|
482
|
+
return
|
|
483
|
+
|
|
484
|
+
# Order update callback
|
|
485
|
+
if self.on_order_update and data.get("type") and data.get("data"):
|
|
486
|
+
self.on_order_update(self, data)
|
|
487
|
+
|
|
488
|
+
def _parse_binary(self, bin):
|
|
489
|
+
"""Parse binary data to a (list of) ticks structure."""
|
|
490
|
+
packets = self._split_packets(bin) # split data to individual ticks packet
|
|
491
|
+
data = []
|
|
492
|
+
|
|
493
|
+
for packet in packets:
|
|
494
|
+
if len(packet) == 19:
|
|
495
|
+
format_string = "<7sid"
|
|
496
|
+
exchange, token, last_trade_price = struct.unpack(format_string, packet)
|
|
497
|
+
exchange = exchange.decode("utf-8").rstrip('\x00')
|
|
498
|
+
data.append({
|
|
499
|
+
"exchange" : exchange,
|
|
500
|
+
"token": token,
|
|
501
|
+
"last_trade_price": last_trade_price
|
|
502
|
+
})
|
|
503
|
+
elif len(packet) == 59:
|
|
504
|
+
format_string = "<7sididdddi"
|
|
505
|
+
exchange, token, last_trade_price, last_trade_time, open_price, high_price, low_price, close_price, volume = struct.unpack(format_string, packet)
|
|
506
|
+
exchange = exchange.decode("utf-8").rstrip('\x00')
|
|
507
|
+
data.append({
|
|
508
|
+
"exchange" : exchange,
|
|
509
|
+
"token": token,
|
|
510
|
+
"last_trade_price": last_trade_price,
|
|
511
|
+
"last_trade_time": last_trade_time,
|
|
512
|
+
"open_price": open_price,
|
|
513
|
+
"high_price": high_price,
|
|
514
|
+
"low_price": low_price,
|
|
515
|
+
"close_price": close_price,
|
|
516
|
+
"volume": volume
|
|
517
|
+
})
|
|
518
|
+
elif len(packet) == 263:
|
|
519
|
+
format_string = "<7siiidiidqqidddddiidiidiidiidiidiidiidiidiidiiii"
|
|
520
|
+
unpacked_data = struct.unpack(format_string, packet)
|
|
521
|
+
exchange = unpacked_data[0].decode("utf-8").rstrip('\x00')
|
|
522
|
+
data.append({
|
|
523
|
+
"exchange" : exchange,
|
|
524
|
+
"token": unpacked_data[1],
|
|
525
|
+
"last_trade_time": unpacked_data[2],
|
|
526
|
+
"last_update_time": unpacked_data[3],
|
|
527
|
+
"last_trade_price": unpacked_data[4],
|
|
528
|
+
"last_trade_quantity": unpacked_data[5],
|
|
529
|
+
"volume": unpacked_data[6],
|
|
530
|
+
"average_trade_price": unpacked_data[7],
|
|
531
|
+
"total_buy_quantity": unpacked_data[8],
|
|
532
|
+
"total_sell_quantity": unpacked_data[9],
|
|
533
|
+
"open_interest": unpacked_data[10],
|
|
534
|
+
"open_price": unpacked_data[11],
|
|
535
|
+
"high_price": unpacked_data[12],
|
|
536
|
+
"low_price": unpacked_data[13],
|
|
537
|
+
"close_price": unpacked_data[14],
|
|
538
|
+
"depth": {
|
|
539
|
+
"buy": [{
|
|
540
|
+
"price": unpacked_data[15],
|
|
541
|
+
"quantity": unpacked_data[16],
|
|
542
|
+
"orders": unpacked_data[17],
|
|
543
|
+
},{
|
|
544
|
+
"price": unpacked_data[18],
|
|
545
|
+
"quantity": unpacked_data[19],
|
|
546
|
+
"orders": unpacked_data[20],
|
|
547
|
+
},{
|
|
548
|
+
"price": unpacked_data[21],
|
|
549
|
+
"quantity": unpacked_data[22],
|
|
550
|
+
"orders": unpacked_data[23],
|
|
551
|
+
},{
|
|
552
|
+
"price": unpacked_data[24],
|
|
553
|
+
"quantity": unpacked_data[25],
|
|
554
|
+
"orders": unpacked_data[26],
|
|
555
|
+
},{
|
|
556
|
+
"price": unpacked_data[27],
|
|
557
|
+
"quantity": unpacked_data[28],
|
|
558
|
+
"orders": unpacked_data[29],
|
|
559
|
+
}],
|
|
560
|
+
"sell": [{
|
|
561
|
+
"price": unpacked_data[30],
|
|
562
|
+
"quantity": unpacked_data[31],
|
|
563
|
+
"orders": unpacked_data[32],
|
|
564
|
+
},{
|
|
565
|
+
"price": unpacked_data[33],
|
|
566
|
+
"quantity": unpacked_data[34],
|
|
567
|
+
"orders": unpacked_data[35],
|
|
568
|
+
},{
|
|
569
|
+
"price": unpacked_data[36],
|
|
570
|
+
"quantity": unpacked_data[37],
|
|
571
|
+
"orders": unpacked_data[38],
|
|
572
|
+
},{
|
|
573
|
+
"price": unpacked_data[39],
|
|
574
|
+
"quantity": unpacked_data[40],
|
|
575
|
+
"orders": unpacked_data[41],
|
|
576
|
+
},{
|
|
577
|
+
"price": unpacked_data[42],
|
|
578
|
+
"quantity": unpacked_data[43],
|
|
579
|
+
"orders": unpacked_data[44],
|
|
580
|
+
}]
|
|
581
|
+
}
|
|
582
|
+
})
|
|
583
|
+
return data
|
|
584
|
+
|
|
585
|
+
def _unpack_int(self, bin, start, end, byte_format="H"):
|
|
586
|
+
"""Unpack binary data as unsgined interger."""
|
|
587
|
+
return struct.unpack("<" + byte_format, bin[start:end])[0]
|
|
588
|
+
|
|
589
|
+
def _split_packets(self, bin):
|
|
590
|
+
"""Split the data to individual packets """
|
|
591
|
+
# Ignore heartbeat data.
|
|
592
|
+
if len(bin) < 2:
|
|
593
|
+
return []
|
|
594
|
+
|
|
595
|
+
number_of_packets = self._unpack_int(bin, 0, 2, byte_format="H")
|
|
596
|
+
packets = []
|
|
597
|
+
|
|
598
|
+
j = 2
|
|
599
|
+
for i in range(number_of_packets):
|
|
600
|
+
packet_length = self._unpack_int(bin, j, j + 2, byte_format="H")
|
|
601
|
+
packets.append(bin[j + 2: j + 2 + packet_length])
|
|
602
|
+
j = j + 2 + packet_length
|
|
603
|
+
|
|
604
|
+
return packets
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: vortex-api
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.6.dev2
|
|
4
4
|
Summary: Vortex APIs to place orders in AsthaTrade Flow application
|
|
5
5
|
Home-page: https://vortex.asthatrade.com
|
|
6
6
|
Download-URL: https://github.com/AsthaTech/pyvortex
|
|
@@ -57,6 +57,32 @@ client.place_order(
|
|
|
57
57
|
client.orders(limit=20,offset=1)
|
|
58
58
|
|
|
59
59
|
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
# Connecting to websocket
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
from vortex_api import VortexFeed
|
|
66
|
+
from vortex_api import Constants as Vc
|
|
67
|
+
|
|
68
|
+
def main():
|
|
69
|
+
# Get access token from any of the login methods
|
|
70
|
+
wire = VortexFeed(access_token)
|
|
71
|
+
|
|
72
|
+
wire.on_price_update = on_price_update
|
|
73
|
+
wire.on_order_update = on_order_update
|
|
74
|
+
wire.on_connect = on_connect
|
|
75
|
+
|
|
76
|
+
def on_price_update(ws,data):
|
|
77
|
+
print(data)
|
|
78
|
+
|
|
79
|
+
def on_order_update(ws,data):
|
|
80
|
+
print(data)
|
|
81
|
+
|
|
82
|
+
def on_connect(ws, response):
|
|
83
|
+
ws.subscribe(Vc.ExchangeTypes.NSE_EQUITY, 26000) # Subscribe to NIFTY 50
|
|
84
|
+
ws.subscribe(Vc.ExchangeTypes.NSE_EQUITY, 26000) # Subscribe to BANKNIFTY
|
|
85
|
+
|
|
60
86
|
```
|
|
61
87
|
Refer to the [python document](https://vortex.asthatrade.com/docs/pyvortex/vortex_api.html) for all methods and features
|
|
62
88
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|