wiz-trader 0.10.0__py3-none-any.whl → 0.11.0__py3-none-any.whl
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.
- wiz_trader/__init__.py +1 -1
- wiz_trader/quotes/client.py +226 -322
- {wiz_trader-0.10.0.dist-info → wiz_trader-0.11.0.dist-info}/METADATA +53 -112
- wiz_trader-0.11.0.dist-info/RECORD +9 -0
- wiz_trader-0.10.0.dist-info/RECORD +0 -9
- {wiz_trader-0.10.0.dist-info → wiz_trader-0.11.0.dist-info}/WHEEL +0 -0
- {wiz_trader-0.10.0.dist-info → wiz_trader-0.11.0.dist-info}/top_level.txt +0 -0
wiz_trader/__init__.py
CHANGED
wiz_trader/quotes/client.py
CHANGED
@@ -3,346 +3,250 @@ import json
|
|
3
3
|
import os
|
4
4
|
import logging
|
5
5
|
import random
|
6
|
-
from typing import Callable, List, Optional, Any,
|
6
|
+
from typing import Callable, List, Optional, Any, Iterator
|
7
7
|
|
8
8
|
import websockets
|
9
9
|
from websockets.exceptions import ConnectionClosed
|
10
10
|
from websockets.protocol import State
|
11
11
|
|
12
|
-
# Setup module
|
12
|
+
# Setup module‐level logger with a default handler if none exists.
|
13
13
|
logger = logging.getLogger(__name__)
|
14
14
|
if not logger.handlers:
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
handler = logging.StreamHandler()
|
16
|
+
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
17
|
+
handler.setFormatter(formatter)
|
18
|
+
logger.addHandler(handler)
|
19
19
|
|
20
20
|
|
21
21
|
class QuotesClient:
|
22
|
-
"""
|
23
|
-
A Python SDK for connecting to the Quotes Server via WebSocket.
|
24
|
-
|
25
|
-
Attributes:
|
26
|
-
base_url (str): WebSocket URL of the quotes server.
|
27
|
-
token (str): JWT token for authentication.
|
28
|
-
log_level (str): Logging level. Options: "error", "info", "debug".
|
29
|
-
"""
|
30
|
-
|
31
|
-
# Constants for actions
|
32
|
-
ACTION_SUBSCRIBE = "subscribe"
|
33
|
-
ACTION_UNSUBSCRIBE = "unsubscribe"
|
34
|
-
|
35
|
-
def __init__(
|
36
|
-
self,
|
37
|
-
base_url: Optional[str] = None,
|
38
|
-
token: Optional[str] = None,
|
39
|
-
log_level: str = "error", # default only errors
|
40
|
-
max_message_size: int = 10 * 1024 * 1024, # 10MB default max size
|
41
|
-
batch_size: int = 20 # Max number of instruments to subscribe to at once
|
42
|
-
):
|
43
|
-
# Configure logger based on log_level.
|
44
|
-
valid_levels = {"error": logging.ERROR, "info": logging.INFO, "debug": logging.DEBUG}
|
45
|
-
if log_level not in valid_levels:
|
46
|
-
raise ValueError(f"log_level must be one of {list(valid_levels.keys())}")
|
47
|
-
logger.setLevel(valid_levels[log_level])
|
48
|
-
|
49
|
-
self.log_level = log_level
|
50
|
-
self.max_message_size = max_message_size
|
51
|
-
self.batch_size = batch_size
|
52
|
-
# System env vars take precedence over .env
|
53
|
-
self.base_url = base_url or os.environ.get("WZ__QUOTES_BASE_URL")
|
54
|
-
self.token = token or os.environ.get("WZ__TOKEN")
|
55
|
-
if not self.token:
|
56
|
-
raise ValueError("JWT token must be provided as an argument or in .env (WZ__TOKEN)")
|
57
|
-
if not self.base_url:
|
58
|
-
raise ValueError("Base URL must be provided as an argument or in .env (WZ__QUOTES_BASE_URL)")
|
59
|
-
|
60
|
-
# Construct the WebSocket URL.
|
61
|
-
self.url = f"{self.base_url}?token={self.token}"
|
62
|
-
self.ws: Optional[websockets.WebSocketClientProtocol] = None
|
63
|
-
self.subscribed_instruments: set = set()
|
64
|
-
self._running = False
|
65
|
-
self._background_task = None
|
66
|
-
|
67
|
-
# Backoff configuration for reconnection (in seconds)
|
68
|
-
self._backoff_base = 1
|
69
|
-
self._backoff_factor = 2
|
70
|
-
self._backoff_max = 60
|
71
|
-
|
72
|
-
# Callbacks
|
73
|
-
self.on_tick: Optional[Callable[[Any, dict], None]] = None
|
74
|
-
self.on_connect: Optional[Callable[[Any], None]] = None
|
75
|
-
self.on_close: Optional[Callable[[Any, Optional[int], Optional[str]], None]] = None
|
76
|
-
self.on_error: Optional[Callable[[Any, Exception], None]] = None
|
77
|
-
|
78
|
-
logger.debug("Initialized QuotesClient with URL: %s", self.url)
|
79
|
-
|
80
|
-
def _chunk_list(self, data: List[Any], chunk_size: int) -> Iterator[List[Any]]:
|
81
22
|
"""
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
""
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
23
|
+
A Python SDK for connecting to the Quotes Server via WebSocket.
|
24
|
+
|
25
|
+
Attributes:
|
26
|
+
base_url (str): WebSocket URL of the quotes server.
|
27
|
+
token (str): JWT token for authentication.
|
28
|
+
log_level (str): Logging level. Options: "error", "info", "debug".
|
29
|
+
"""
|
30
|
+
|
31
|
+
ACTION_SUBSCRIBE = "subscribe"
|
32
|
+
ACTION_UNSUBSCRIBE = "unsubscribe"
|
33
|
+
|
34
|
+
def __init__(
|
35
|
+
self,
|
36
|
+
base_url: Optional[str] = None,
|
37
|
+
token: Optional[str] = None,
|
38
|
+
log_level: str = "error",
|
39
|
+
max_message_size: int = 10 * 1024 * 1024,
|
40
|
+
batch_size: int = 20
|
41
|
+
):
|
42
|
+
valid_levels = {"error": logging.ERROR, "info": logging.INFO, "debug": logging.DEBUG}
|
43
|
+
if log_level not in valid_levels:
|
44
|
+
raise ValueError(f"log_level must be one of {list(valid_levels.keys())}")
|
45
|
+
logger.setLevel(valid_levels[log_level])
|
46
|
+
|
47
|
+
self.log_level = log_level
|
48
|
+
self.max_message_size = max_message_size
|
49
|
+
self.batch_size = batch_size
|
50
|
+
|
51
|
+
self.base_url = base_url or os.environ.get("WZ__QUOTES_BASE_URL")
|
52
|
+
self.token = token or os.environ.get("WZ__TOKEN")
|
53
|
+
if not self.token:
|
54
|
+
raise ValueError("JWT token must be provided as an argument or in .env (WZ__TOKEN)")
|
55
|
+
if not self.base_url:
|
56
|
+
raise ValueError("Base URL must be provided as an argument or in .env (WZ__QUOTES_BASE_URL)")
|
57
|
+
|
58
|
+
self.url = f"{self.base_url}?token={self.token}"
|
59
|
+
self.ws: Optional[websockets.WebSocketClientProtocol] = None
|
60
|
+
self.subscribed_instruments: set = set()
|
61
|
+
self._running = False
|
62
|
+
self._background_task = None
|
63
|
+
|
64
|
+
self._backoff_base = 1
|
65
|
+
self._backoff_factor = 2
|
66
|
+
self._backoff_max = 60
|
67
|
+
|
68
|
+
# Callbacks are plain synchronous functions
|
69
|
+
self.on_tick: Optional[Callable[[Any, dict], None]] = None
|
70
|
+
self.on_connect: Optional[Callable[[Any], None]] = None
|
71
|
+
self.on_close: Optional[Callable[[Any, Optional[int], Optional[str]], None]] = None
|
72
|
+
self.on_error: Optional[Callable[[Any, Exception], None]] = None
|
73
|
+
|
74
|
+
logger.debug("Initialized QuotesClient with URL: %s", self.url)
|
75
|
+
|
76
|
+
def _chunk_list(self, data: List[Any], chunk_size: int) -> Iterator[List[Any]]:
|
77
|
+
for i in range(0, len(data), chunk_size):
|
78
|
+
yield data[i:i + chunk_size]
|
79
|
+
|
80
|
+
async def _connect_with_backoff(self) -> None:
|
81
|
+
backoff = self._backoff_base
|
82
|
+
|
83
|
+
while self._running:
|
109
84
|
try:
|
110
|
-
|
85
|
+
logger.info("Connecting to %s ...", self.url)
|
86
|
+
async with websockets.connect(self.url, max_size=self.max_message_size) as websocket:
|
87
|
+
self.ws = websocket
|
88
|
+
logger.info("Connected to the quotes server.")
|
89
|
+
|
90
|
+
# plain sync on_connect
|
91
|
+
if self.on_connect:
|
92
|
+
try:
|
93
|
+
self.on_connect(self)
|
94
|
+
except Exception as e:
|
95
|
+
logger.error("Error in on_connect callback: %s", e, exc_info=True)
|
96
|
+
|
97
|
+
# re-subscribe on reconnect
|
98
|
+
if self.subscribed_instruments:
|
99
|
+
for batch in self._chunk_list(list(self.subscribed_instruments), self.batch_size):
|
100
|
+
msg = {"action": self.ACTION_SUBSCRIBE, "instruments": batch}
|
101
|
+
await self.ws.send(json.dumps(msg))
|
102
|
+
logger.info("Re-subscribed to %d instruments", len(batch))
|
103
|
+
await asyncio.sleep(0.1)
|
104
|
+
|
105
|
+
backoff = self._backoff_base
|
106
|
+
await self._handle_messages()
|
107
|
+
|
108
|
+
except ConnectionClosed as e:
|
109
|
+
logger.info("Disconnected: %s", e)
|
110
|
+
if self.on_close:
|
111
|
+
try:
|
112
|
+
self.on_close(self, getattr(e, 'code', None), str(e))
|
113
|
+
except Exception as ex:
|
114
|
+
logger.error("Error in on_close callback: %s", ex, exc_info=True)
|
115
|
+
|
111
116
|
except Exception as e:
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
# Reset backoff after a successful connection.
|
129
|
-
backoff = self._backoff_base
|
130
|
-
|
131
|
-
await self._handle_messages()
|
132
|
-
except ConnectionClosed as e:
|
133
|
-
logger.info("Disconnected from the quotes server: %s", e)
|
134
|
-
# Call the on_close callback if provided
|
135
|
-
if self.on_close:
|
136
|
-
try:
|
137
|
-
self.on_close(self, getattr(e, 'code', None), str(e))
|
138
|
-
except Exception as e:
|
139
|
-
logger.error("Error in on_close callback: %s", e, exc_info=True)
|
140
|
-
except Exception as e:
|
141
|
-
logger.error("Connection error: %s", e, exc_info=True)
|
142
|
-
# Call the on_error callback if provided
|
143
|
-
if self.on_error:
|
144
|
-
try:
|
145
|
-
self.on_error(self, e)
|
146
|
-
except Exception as e:
|
147
|
-
logger.error("Error in on_error callback: %s", e, exc_info=True)
|
148
|
-
|
149
|
-
# Don't reconnect if we're no longer running
|
150
|
-
if not self._running:
|
151
|
-
break
|
152
|
-
|
153
|
-
# Exponential backoff before reconnecting.
|
154
|
-
sleep_time = min(backoff, self._backoff_max)
|
155
|
-
logger.info("Reconnecting in %s seconds...", sleep_time)
|
156
|
-
await asyncio.sleep(sleep_time)
|
157
|
-
backoff *= self._backoff_factor
|
158
|
-
# Add a bit of randomness to avoid thundering herd issues.
|
159
|
-
backoff += random.uniform(0, 1)
|
160
|
-
|
161
|
-
async def _handle_messages(self) -> None:
|
162
|
-
"""
|
163
|
-
Handle incoming messages and dispatch them via the on_tick callback.
|
164
|
-
Handles newline-delimited JSON objects in a single message.
|
165
|
-
"""
|
166
|
-
try:
|
167
|
-
async for message in self.ws: # type: ignore
|
168
|
-
# Log message size for debugging large message issues
|
169
|
-
if self.log_level == "debug" and isinstance(message, str):
|
170
|
-
message_size = len(message.encode("utf-8"))
|
171
|
-
if message_size > 1024 * 1024: # Over 1MB
|
172
|
-
logger.debug("Received large message: %d bytes", message_size)
|
173
|
-
# Log the beginning of the message for debugging
|
174
|
-
logger.debug("Message starts with: %s", message[:100])
|
175
|
-
|
176
|
-
if isinstance(message, str):
|
177
|
-
# Special handling for newline-delimited JSON
|
178
|
-
if '\n' in message:
|
179
|
-
# Split by newlines and process each JSON object separately
|
180
|
-
for json_str in message.strip().split('\n'):
|
181
|
-
if not json_str:
|
182
|
-
continue
|
183
|
-
|
184
|
-
try:
|
185
|
-
tick = json.loads(json_str)
|
186
|
-
if self.on_tick:
|
187
|
-
self.on_tick(self, tick)
|
188
|
-
except json.JSONDecodeError as e:
|
189
|
-
logger.error("Failed to parse JSON object: %s", str(e))
|
190
|
-
logger.error("Invalid JSON: %s...", json_str[:100])
|
191
|
-
else:
|
192
|
-
# Single JSON object
|
193
|
-
try:
|
194
|
-
tick = json.loads(message)
|
195
|
-
if self.on_tick:
|
196
|
-
self.on_tick(self, tick)
|
197
|
-
except json.JSONDecodeError as e:
|
198
|
-
logger.error("Failed to parse JSON: %s", str(e))
|
199
|
-
logger.error("Invalid JSON message: %s...", message[:100])
|
200
|
-
else:
|
201
|
-
logger.warning("Received non-string message: %s", type(message))
|
202
|
-
except ConnectionClosed as e:
|
203
|
-
logger.info("Connection closed during message handling: %s", e)
|
204
|
-
# Let the _connect_with_backoff method handle reconnection
|
205
|
-
except Exception as e:
|
206
|
-
logger.error("Error processing message: %s", str(e), exc_info=True)
|
207
|
-
# Call the on_error callback if provided
|
208
|
-
if self.on_error:
|
117
|
+
logger.error("Connection error: %s", e, exc_info=True)
|
118
|
+
if self.on_error:
|
119
|
+
try:
|
120
|
+
self.on_error(self, e)
|
121
|
+
except Exception as ex:
|
122
|
+
logger.error("Error in on_error callback: %s", ex, exc_info=True)
|
123
|
+
|
124
|
+
if not self._running:
|
125
|
+
break
|
126
|
+
|
127
|
+
sleep_time = min(backoff, self._backoff_max)
|
128
|
+
logger.info("Reconnecting in %s seconds...", sleep_time)
|
129
|
+
await asyncio.sleep(sleep_time)
|
130
|
+
backoff = backoff * self._backoff_factor + random.uniform(0, 1)
|
131
|
+
|
132
|
+
async def _handle_messages(self) -> None:
|
209
133
|
try:
|
210
|
-
|
134
|
+
async for message in self.ws: # type: ignore
|
135
|
+
if self.log_level == "debug" and isinstance(message, str):
|
136
|
+
size = len(message.encode("utf-8"))
|
137
|
+
if size > 1024 * 1024:
|
138
|
+
logger.debug("Large message: %d bytes", size)
|
139
|
+
|
140
|
+
if isinstance(message, str):
|
141
|
+
for chunk in message.strip().split("\n"):
|
142
|
+
if not chunk:
|
143
|
+
continue
|
144
|
+
try:
|
145
|
+
tick = json.loads(chunk)
|
146
|
+
if self.on_tick:
|
147
|
+
self.on_tick(self, tick)
|
148
|
+
except json.JSONDecodeError as e:
|
149
|
+
logger.error("JSON parse error: %s", e)
|
150
|
+
else:
|
151
|
+
logger.warning("Non-string message: %s", type(message))
|
152
|
+
except ConnectionClosed:
|
153
|
+
logger.info("Connection closed during message handling")
|
211
154
|
except Exception as e:
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
155
|
+
logger.error("Error processing message: %s", e, exc_info=True)
|
156
|
+
if self.on_error:
|
157
|
+
try:
|
158
|
+
self.on_error(self, e)
|
159
|
+
except Exception:
|
160
|
+
pass
|
161
|
+
|
162
|
+
# -- Async core methods (for internal use) --
|
163
|
+
|
164
|
+
async def _subscribe_async(self, instruments: List[str]) -> None:
|
165
|
+
"""
|
166
|
+
Internal async subscription. Use `subscribe()` wrapper to schedule this.
|
167
|
+
"""
|
168
|
+
if self.ws and self.ws.state == State.OPEN:
|
169
|
+
new = set(instruments) - self.subscribed_instruments
|
170
|
+
if new:
|
171
|
+
self.subscribed_instruments |= new
|
172
|
+
for batch in self._chunk_list(list(new), self.batch_size):
|
173
|
+
logger.info("Subscribing to %d instruments", len(batch))
|
174
|
+
await self.ws.send(json.dumps({
|
175
|
+
"action": self.ACTION_SUBSCRIBE,
|
176
|
+
"instruments": batch
|
177
|
+
}))
|
178
|
+
await asyncio.sleep(0.1)
|
179
|
+
else:
|
180
|
+
# queue it for next connect
|
181
|
+
self.subscribed_instruments |= set(instruments)
|
217
182
|
|
218
|
-
|
219
|
-
instruments (List[str]): List of instrument identifiers.
|
220
|
-
"""
|
221
|
-
if self.ws and self.ws.state == State.OPEN:
|
222
|
-
new_instruments = set(instruments) - self.subscribed_instruments
|
223
|
-
if new_instruments:
|
224
|
-
self.subscribed_instruments.update(new_instruments)
|
225
|
-
|
226
|
-
# Split into batches to avoid message size issues
|
227
|
-
new_instruments_list = list(new_instruments)
|
228
|
-
for batch in self._chunk_list(new_instruments_list, self.batch_size):
|
229
|
-
logger.info("Subscribing to batch of %d instruments", len(batch))
|
230
|
-
message = {"action": self.ACTION_SUBSCRIBE, "instruments": batch}
|
231
|
-
await self.ws.send(json.dumps(message))
|
232
|
-
# Small delay between batches to avoid overwhelming the server
|
233
|
-
await asyncio.sleep(0.1)
|
234
|
-
|
235
|
-
logger.info("Completed subscription for %d new instruments", len(new_instruments))
|
236
|
-
else:
|
237
|
-
logger.info("Instruments already subscribed: %s", instruments)
|
238
|
-
else:
|
239
|
-
logger.info("Cannot subscribe: WebSocket is not connected.")
|
240
|
-
# Still update the subscription list so we can subscribe when connected
|
241
|
-
self.subscribed_instruments.update(set(instruments))
|
242
|
-
|
243
|
-
async def unsubscribe(self, instruments: List[str]) -> None:
|
244
|
-
"""
|
245
|
-
Unsubscribe from a list of instruments.
|
183
|
+
# -- Public wrappers for plain callback users --
|
246
184
|
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
logger.info("Cannot unsubscribe: WebSocket is not connected.")
|
271
|
-
# Still update the subscription list
|
272
|
-
self.subscribed_instruments.difference_update(set(instruments))
|
273
|
-
|
274
|
-
async def close(self) -> None:
|
275
|
-
"""
|
276
|
-
Close the WebSocket connection.
|
277
|
-
"""
|
278
|
-
self._running = False
|
279
|
-
if self.ws:
|
280
|
-
await self.ws.close()
|
281
|
-
logger.info("WebSocket connection closed.")
|
282
|
-
|
283
|
-
# Cancel the background task if it exists
|
284
|
-
if self._background_task and not self._background_task.done():
|
285
|
-
self._background_task.cancel()
|
286
|
-
try:
|
287
|
-
await self._background_task
|
288
|
-
except asyncio.CancelledError:
|
289
|
-
pass
|
290
|
-
|
291
|
-
def connect(self) -> None:
|
292
|
-
"""
|
293
|
-
Connect to the websocket server in a blocking manner.
|
294
|
-
This method handles the event loop and will not return until stop() is called.
|
295
|
-
|
296
|
-
Similar to KiteTicker's connect() method, this creates and runs the event loop.
|
297
|
-
"""
|
298
|
-
self._running = True
|
299
|
-
|
300
|
-
# If there's already an event loop, use it
|
301
|
-
try:
|
302
|
-
loop = asyncio.get_event_loop()
|
303
|
-
except RuntimeError:
|
304
|
-
# No event loop exists, create a new one
|
305
|
-
loop = asyncio.new_event_loop()
|
306
|
-
asyncio.set_event_loop(loop)
|
307
|
-
|
308
|
-
try:
|
309
|
-
# Run until completion (i.e., until stop() is called)
|
310
|
-
loop.run_until_complete(self._connect_with_backoff())
|
311
|
-
finally:
|
312
|
-
if not loop.is_closed():
|
313
|
-
# Clean up pending tasks
|
314
|
-
pending = asyncio.all_tasks(loop)
|
315
|
-
for task in pending:
|
316
|
-
task.cancel()
|
317
|
-
|
318
|
-
# Run until all tasks are properly canceled
|
185
|
+
def subscribe(self, instruments: List[str]) -> None:
|
186
|
+
"""
|
187
|
+
Schedule an async subscribe so users can call this without 'await'.
|
188
|
+
"""
|
189
|
+
try:
|
190
|
+
loop = asyncio.get_event_loop()
|
191
|
+
except RuntimeError:
|
192
|
+
loop = asyncio.new_event_loop()
|
193
|
+
asyncio.set_event_loop(loop)
|
194
|
+
loop.create_task(self._subscribe_async(instruments))
|
195
|
+
|
196
|
+
# You could add a similar unsubscribe wrapper if needed
|
197
|
+
|
198
|
+
async def close(self) -> None:
|
199
|
+
"""
|
200
|
+
Close the WebSocket connection.
|
201
|
+
"""
|
202
|
+
self._running = False
|
203
|
+
if self.ws:
|
204
|
+
await self.ws.close()
|
205
|
+
logger.info("WebSocket closed.")
|
206
|
+
if self._background_task and not self._background_task.done():
|
207
|
+
self._background_task.cancel()
|
319
208
|
try:
|
320
|
-
|
321
|
-
except
|
209
|
+
await self._background_task
|
210
|
+
except asyncio.CancelledError:
|
322
211
|
pass
|
323
212
|
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
213
|
+
def connect(self) -> None:
|
214
|
+
"""
|
215
|
+
Blocking connect (runs the internal asyncio loop until stop()).
|
216
|
+
"""
|
217
|
+
self._running = True
|
218
|
+
try:
|
219
|
+
loop = asyncio.get_event_loop()
|
220
|
+
except RuntimeError:
|
221
|
+
loop = asyncio.new_event_loop()
|
222
|
+
asyncio.set_event_loop(loop)
|
223
|
+
|
224
|
+
try:
|
225
|
+
loop.run_until_complete(self._connect_with_backoff())
|
226
|
+
finally:
|
227
|
+
if not loop.is_closed():
|
228
|
+
tasks = asyncio.all_tasks(loop)
|
229
|
+
for t in tasks:
|
230
|
+
t.cancel()
|
231
|
+
try:
|
232
|
+
loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True))
|
233
|
+
except Exception:
|
234
|
+
pass
|
235
|
+
|
236
|
+
def connect_async(self) -> None:
|
237
|
+
"""
|
238
|
+
Non-blocking connect: starts the background task.
|
239
|
+
"""
|
240
|
+
if self._running:
|
241
|
+
logger.warning("Client already running.")
|
242
|
+
return
|
243
|
+
self._running = True
|
335
244
|
loop = asyncio.get_event_loop()
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
Stop the websocket connection.
|
345
|
-
This is a non-blocking method that just flags the client to stop.
|
346
|
-
"""
|
347
|
-
self._running = False
|
348
|
-
logger.info("Client stopping. Connection will close soon.")
|
245
|
+
self._background_task = loop.create_task(self._connect_with_backoff())
|
246
|
+
|
247
|
+
def stop(self) -> None:
|
248
|
+
"""
|
249
|
+
Signal the client to stop and close.
|
250
|
+
"""
|
251
|
+
self._running = False
|
252
|
+
logger.info("Client stopping; will close soon.")
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: wiz_trader
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.11.0
|
4
4
|
Summary: A Python SDK for connecting to the Wizzer.
|
5
5
|
Home-page: https://bitbucket.org/wizzer-tech/quotes_sdk.git
|
6
6
|
Author: Pawan Wagh
|
@@ -132,7 +132,7 @@ quotes_client = QuotesClient(log_level="debug") # Only override log level
|
|
132
132
|
|
133
133
|
## Quotes Client
|
134
134
|
|
135
|
-
The `QuotesClient` enables you to connect to Wizzer's WebSocket server for real
|
135
|
+
The `QuotesClient` enables you to connect to Wizzer's WebSocket server for real‑time market data using **plain synchronous** callbacks—no `async def` required.
|
136
136
|
|
137
137
|
### Quotes Client Initialization
|
138
138
|
|
@@ -145,214 +145,155 @@ from wiz_trader import QuotesClient
|
|
145
145
|
client = QuotesClient(
|
146
146
|
base_url="wss://websocket-url/quotes",
|
147
147
|
token="your-jwt-token",
|
148
|
-
log_level="info",
|
149
|
-
max_message_size=10 * 1024 * 1024, # Optional:
|
150
|
-
batch_size=20
|
148
|
+
log_level="info", # Options: "error", "info", "debug"
|
149
|
+
max_message_size=10 * 1024 * 1024, # Optional: default 10MB
|
150
|
+
batch_size=20 # Optional: default 20 instruments per batch
|
151
151
|
)
|
152
152
|
|
153
153
|
# Method 2: Using environment variables
|
154
|
-
# (Requires WZ__QUOTES_BASE_URL and WZ__TOKEN
|
154
|
+
# (Requires WZ__QUOTES_BASE_URL and WZ__TOKEN set)
|
155
155
|
client = QuotesClient(log_level="info")
|
156
156
|
|
157
|
-
# Method 3: Mixed approach
|
157
|
+
# Method 3: Mixed approach
|
158
158
|
client = QuotesClient(
|
159
|
-
base_url="wss://custom-
|
160
|
-
log_level="debug"
|
161
|
-
# token will be taken from WZ__TOKEN environment variable
|
159
|
+
base_url="wss://custom-url/quotes",
|
160
|
+
log_level="debug" # token from WZ__TOKEN env var
|
162
161
|
)
|
163
162
|
```
|
164
163
|
|
165
164
|
### Connection Methods
|
166
165
|
|
167
|
-
The `QuotesClient` offers two ways to connect:
|
168
|
-
|
169
166
|
#### Blocking Connection
|
170
167
|
|
171
|
-
|
168
|
+
Blocks your main thread, similar to Zerodha’s KiteTicker:
|
172
169
|
|
173
170
|
```python
|
174
|
-
# This will block and run until stopped
|
175
171
|
client.connect()
|
176
172
|
```
|
177
173
|
|
178
|
-
#### Non
|
174
|
+
#### Non‑Blocking Connection
|
179
175
|
|
180
|
-
|
176
|
+
Run alongside your own `asyncio` code:
|
181
177
|
|
182
178
|
```python
|
183
|
-
# Start the connection in the background
|
184
179
|
client.connect_async()
|
185
|
-
|
186
|
-
# Later, when you want to stop:
|
180
|
+
# … do other work …
|
187
181
|
client.stop()
|
188
182
|
```
|
189
183
|
|
190
184
|
### Callbacks
|
191
185
|
|
192
|
-
|
186
|
+
All callbacks are **plain `def`** functions. Inside them you can call `subscribe(...)`, which under the hood schedules the actual async work—so you never `await` in your callbacks.
|
193
187
|
|
194
188
|
```python
|
195
189
|
def on_tick(ws, tick):
|
196
|
-
""
|
197
|
-
print(f"Received tick: {tick}")
|
190
|
+
print("Tick:", tick)
|
198
191
|
|
199
192
|
def on_connect(ws):
|
200
|
-
""
|
201
|
-
|
202
|
-
|
203
|
-
|
193
|
+
print("Connected!")
|
194
|
+
# fire‑and‑forget subscribe—no await needed
|
195
|
+
ws.subscribe([
|
196
|
+
"NSE:SBIN:3045",
|
197
|
+
"NSE:RELIANCE:2885"
|
198
|
+
])
|
204
199
|
|
205
200
|
def on_close(ws, code, reason):
|
206
|
-
"
|
207
|
-
|
208
|
-
# Optional: Stop the client (if you want to exit)
|
209
|
-
# ws.stop()
|
201
|
+
print(f"Connection closed [{code}]: {reason}")
|
202
|
+
ws.stop() # to prevent auto‑reconnect
|
210
203
|
|
211
204
|
def on_error(ws, error):
|
212
|
-
""
|
213
|
-
print(f"Error: {error}")
|
205
|
+
print("Error:", error)
|
214
206
|
|
215
|
-
|
216
|
-
client.on_tick = on_tick
|
207
|
+
client.on_tick = on_tick
|
217
208
|
client.on_connect = on_connect
|
218
|
-
client.on_close
|
219
|
-
client.on_error
|
209
|
+
client.on_close = on_close
|
210
|
+
client.on_error = on_error
|
220
211
|
```
|
221
212
|
|
222
213
|
### Subscribing to Instruments
|
223
214
|
|
224
|
-
|
215
|
+
Call `ws.subscribe([...])` directly; the SDK will batch large lists and send them over the socket:
|
225
216
|
|
226
217
|
```python
|
227
|
-
# Inside an on_connect callback:
|
228
218
|
def on_connect(ws):
|
229
|
-
#
|
230
|
-
ws.subscribe(["NSE:SBIN:3045"])
|
231
|
-
|
232
|
-
# Or subscribe to multiple instruments at once
|
219
|
+
# subscribe without await
|
233
220
|
ws.subscribe([
|
234
221
|
"NSE:SBIN:3045",
|
235
222
|
"NSE:ICICIBANK:4963",
|
236
|
-
"NSE:RELIANCE:2885"
|
223
|
+
"NSE:RELIANCE:2885",
|
237
224
|
])
|
238
225
|
```
|
239
226
|
|
240
|
-
For large lists of instruments, the client will automatically batch them to avoid message size issues.
|
241
|
-
|
242
227
|
### Unsubscribing from Instruments
|
243
228
|
|
244
|
-
|
229
|
+
Similarly, plain call to `unsubscribe`:
|
245
230
|
|
246
231
|
```python
|
247
|
-
# Unsubscribe from specific instruments
|
248
232
|
ws.unsubscribe(["NSE:SBIN:3045", "NSE:ICICIBANK:4963"])
|
249
233
|
```
|
250
234
|
|
251
|
-
###
|
252
|
-
|
253
|
-
The WebSocket connection automatically attempts to reconnect with exponential backoff if disconnected. You don't need to handle this manually.
|
254
|
-
|
255
|
-
To explicitly close the connection:
|
256
|
-
|
257
|
-
```python
|
258
|
-
# In a blocking context:
|
259
|
-
client.stop()
|
260
|
-
|
261
|
-
# In an async context:
|
262
|
-
await client.close()
|
263
|
-
```
|
264
|
-
|
265
|
-
### Quotes Client Examples
|
235
|
+
### Complete Examples
|
266
236
|
|
267
|
-
####
|
237
|
+
#### Blocking Example
|
268
238
|
|
269
239
|
```python
|
270
240
|
import logging
|
271
241
|
from wiz_trader import QuotesClient
|
272
242
|
|
273
|
-
|
274
|
-
logging.basicConfig(level=logging.DEBUG)
|
243
|
+
logging.basicConfig(level=logging.INFO)
|
275
244
|
|
276
|
-
# Initialize the client
|
277
245
|
client = QuotesClient(
|
278
246
|
base_url="wss://websocket-url/quotes",
|
279
247
|
token="your-jwt-token"
|
280
248
|
)
|
281
249
|
|
282
250
|
def on_tick(ws, tick):
|
283
|
-
""
|
284
|
-
logging.debug("Received tick: %s", tick)
|
251
|
+
logging.debug("Tick: %s", tick)
|
285
252
|
|
286
253
|
def on_connect(ws):
|
287
|
-
""
|
288
|
-
|
289
|
-
# Subscribe to instruments
|
290
|
-
ws.subscribe(["NSE:SBIN:3045", "NSE:RELIANCE:2885"])
|
254
|
+
logging.info("Connected.")
|
255
|
+
ws.subscribe(["NSE:SBIN:3045", "NSE:RELIANCE:2885"]) # no await
|
291
256
|
|
292
257
|
def on_close(ws, code, reason):
|
293
|
-
""
|
294
|
-
|
258
|
+
logging.warning("Closed: %s", reason)
|
259
|
+
ws.stop()
|
295
260
|
|
296
261
|
def on_error(ws, error):
|
297
|
-
""
|
298
|
-
logging.error("Error occurred: %s", error)
|
262
|
+
logging.error("Error: %s", error)
|
299
263
|
|
300
|
-
|
301
|
-
client.on_tick = on_tick
|
264
|
+
client.on_tick = on_tick
|
302
265
|
client.on_connect = on_connect
|
303
|
-
client.on_close
|
304
|
-
client.on_error
|
266
|
+
client.on_close = on_close
|
267
|
+
client.on_error = on_error
|
305
268
|
|
306
|
-
# Connect and run (blocking call)
|
307
269
|
try:
|
308
270
|
client.connect()
|
309
271
|
except KeyboardInterrupt:
|
310
|
-
print("Interrupted by user, shutting down...")
|
311
272
|
client.stop()
|
312
273
|
```
|
313
274
|
|
314
|
-
#### Non
|
275
|
+
#### Non‑Blocking Example
|
315
276
|
|
316
277
|
```python
|
317
278
|
import asyncio
|
318
279
|
import logging
|
319
|
-
from wiz_trader import QuotesClient
|
280
|
+
from wiz_trader import QuotesClient
|
320
281
|
|
321
282
|
async def main():
|
322
|
-
# Configure logging
|
323
283
|
logging.basicConfig(level=logging.INFO)
|
324
|
-
|
325
|
-
# Initialize the quotes client
|
326
|
-
quotes_client = QuotesClient(
|
284
|
+
client = QuotesClient(
|
327
285
|
base_url="wss://websocket-url/quotes",
|
328
286
|
token="your-jwt-token"
|
329
287
|
)
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
)
|
336
|
-
|
337
|
-
# Set up quotes callbacks
|
338
|
-
quotes_client.on_tick = lambda ws, tick: logging.info(f"Tick: {tick}")
|
339
|
-
quotes_client.on_connect = lambda ws: ws.subscribe(["NSE:SBIN:3045"])
|
340
|
-
|
341
|
-
# Connect in non-blocking mode
|
342
|
-
quotes_client.connect_async()
|
343
|
-
|
344
|
-
# Perform other operations
|
345
|
-
indices = await asyncio.to_thread(wizzer_client.get_indices, exchange="NSE")
|
346
|
-
logging.info(f"Available indices: {[idx['name'] for idx in indices[:5]]}")
|
347
|
-
|
348
|
-
# Wait for some time
|
288
|
+
client.on_tick = lambda ws, tick: logging.info(tick)
|
289
|
+
client.on_connect = lambda ws: ws.subscribe(["NSE:SBIN:3045"])
|
290
|
+
client.connect_async()
|
291
|
+
|
292
|
+
# Your other async work here...
|
349
293
|
await asyncio.sleep(60)
|
350
|
-
|
351
|
-
# Stop the quotes client
|
352
|
-
quotes_client.stop()
|
294
|
+
client.stop()
|
353
295
|
|
354
|
-
|
355
|
-
asyncio.run(main())
|
296
|
+
asyncio.run(main())
|
356
297
|
```
|
357
298
|
|
358
299
|
## Wizzer Client
|
@@ -0,0 +1,9 @@
|
|
1
|
+
wiz_trader/__init__.py,sha256=C0FSsZWVvQ5bLn9WzjVb3QD4erLfxjy5V0bCAjxBECE,182
|
2
|
+
wiz_trader/apis/__init__.py,sha256=ItWKMOl4omiW0g2f-M7WRW3v-dss_ULd9vYnFyIIT9o,132
|
3
|
+
wiz_trader/apis/client.py,sha256=4Vu9z0cLjJ62LTNT13UwicU0Ez3jnwQOcirWtJAF4Hw,25578
|
4
|
+
wiz_trader/quotes/__init__.py,sha256=RF9g9CNP6bVWlmCh_ad8krm3-EWOIuVfLp0-H9fAeEM,108
|
5
|
+
wiz_trader/quotes/client.py,sha256=LJeMcQPjJIRxrTIGalWsLYh_XfinDXBP5-4cNS7qCxc,9709
|
6
|
+
wiz_trader-0.11.0.dist-info/METADATA,sha256=IpTpq7bUvtWlCM1f-BVOx9K2MTM_HCsLXd_T5aefXKU,53081
|
7
|
+
wiz_trader-0.11.0.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
8
|
+
wiz_trader-0.11.0.dist-info/top_level.txt,sha256=lnYS_g8LlA6ryKYnvY8xIQ6K2K-xzOsd-99AWgnW6VY,11
|
9
|
+
wiz_trader-0.11.0.dist-info/RECORD,,
|
@@ -1,9 +0,0 @@
|
|
1
|
-
wiz_trader/__init__.py,sha256=ZN7H57N8SfZmgvWqH-nXE5wK5wAMUBkMMhMFkkfXWI0,182
|
2
|
-
wiz_trader/apis/__init__.py,sha256=ItWKMOl4omiW0g2f-M7WRW3v-dss_ULd9vYnFyIIT9o,132
|
3
|
-
wiz_trader/apis/client.py,sha256=4Vu9z0cLjJ62LTNT13UwicU0Ez3jnwQOcirWtJAF4Hw,25578
|
4
|
-
wiz_trader/quotes/__init__.py,sha256=RF9g9CNP6bVWlmCh_ad8krm3-EWOIuVfLp0-H9fAeEM,108
|
5
|
-
wiz_trader/quotes/client.py,sha256=4djU1YgXuyhsWW-bgkI46BzK31PJlcrKXtxViVusEtY,13192
|
6
|
-
wiz_trader-0.10.0.dist-info/METADATA,sha256=Cn4OhgEZtR9ih-7S2RuCiH7l3hcTF2-yWHZ05uojhF8,54894
|
7
|
-
wiz_trader-0.10.0.dist-info/WHEEL,sha256=pxyMxgL8-pra_rKaQ4drOZAegBVuX-G_4nRHjjgWbmo,91
|
8
|
-
wiz_trader-0.10.0.dist-info/top_level.txt,sha256=lnYS_g8LlA6ryKYnvY8xIQ6K2K-xzOsd-99AWgnW6VY,11
|
9
|
-
wiz_trader-0.10.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|