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 CHANGED
@@ -3,6 +3,6 @@
3
3
  from .quotes import QuotesClient
4
4
  from .apis import WizzerClient
5
5
 
6
- __version__ = "0.10.0"
6
+ __version__ = "0.11.0"
7
7
 
8
8
  __all__ = ["QuotesClient", "WizzerClient"]
@@ -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, Dict, Iterator
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-level logger with a default handler if none exists.
12
+ # Setup modulelevel logger with a default handler if none exists.
13
13
  logger = logging.getLogger(__name__)
14
14
  if not logger.handlers:
15
- handler = logging.StreamHandler()
16
- formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
17
- handler.setFormatter(formatter)
18
- logger.addHandler(handler)
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
- Split a list into smaller chunks.
83
-
84
- Args:
85
- data (List[Any]): The list to split.
86
- chunk_size (int): Maximum size of each chunk.
87
-
88
- Returns:
89
- Iterator[List[Any]]: Iterator of list chunks.
90
- """
91
- for i in range(0, len(data), chunk_size):
92
- yield data[i:i + chunk_size]
93
-
94
- async def _connect_with_backoff(self) -> None:
95
- """
96
- Continuously connect to the quotes server with exponential backoff.
97
- """
98
- backoff = self._backoff_base
99
-
100
- while self._running:
101
- try:
102
- logger.info("Connecting to %s ...", self.url)
103
- async with websockets.connect(self.url, max_size=self.max_message_size) as websocket:
104
- self.ws = websocket
105
- logger.info("Connected to the quotes server.")
106
-
107
- # Call the on_connect callback if provided
108
- if self.on_connect:
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
- self.on_connect(self)
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
- logger.error("Error in on_connect callback: %s", e, exc_info=True)
113
-
114
- # On reconnection, re-subscribe if needed.
115
- if self.subscribed_instruments:
116
- # Split into batches to avoid message size issues
117
- instruments_list = list(self.subscribed_instruments)
118
- for batch in self._chunk_list(instruments_list, self.batch_size):
119
- subscribe_msg = {
120
- "action": self.ACTION_SUBSCRIBE,
121
- "instruments": batch
122
- }
123
- await self.ws.send(json.dumps(subscribe_msg))
124
- logger.info("Re-subscribed to batch of %d instruments", len(batch))
125
- # Small delay between batches to avoid overwhelming the server
126
- await asyncio.sleep(0.1)
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
- self.on_error(self, e)
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
- logger.error("Error in on_error callback: %s", e, exc_info=True)
213
-
214
- async def subscribe(self, instruments: List[str]) -> None:
215
- """
216
- Subscribe to a list of instruments.
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
- Args:
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
- Args:
248
- instruments (List[str]): List of instrument identifiers.
249
- """
250
- if self.ws and self.ws.state == State.OPEN:
251
- unsub_set = set(instruments)
252
- to_unsubscribe = unsub_set & self.subscribed_instruments
253
-
254
- if to_unsubscribe:
255
- self.subscribed_instruments.difference_update(to_unsubscribe)
256
-
257
- # Split into batches to avoid message size issues
258
- unsub_list = list(to_unsubscribe)
259
- for batch in self._chunk_list(unsub_list, self.batch_size):
260
- logger.info("Unsubscribing from batch of %d instruments", len(batch))
261
- message = {"action": self.ACTION_UNSUBSCRIBE, "instruments": batch}
262
- await self.ws.send(json.dumps(message))
263
- # Small delay between batches to avoid overwhelming the server
264
- await asyncio.sleep(0.1)
265
-
266
- logger.info("Completed unsubscription for %d instruments", len(to_unsubscribe))
267
- else:
268
- logger.info("No matching instruments found in current subscription.")
269
- else:
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
- loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
321
- except Exception:
209
+ await self._background_task
210
+ except asyncio.CancelledError:
322
211
  pass
323
212
 
324
- def connect_async(self) -> None:
325
- """
326
- Connect to the websocket server in a non-blocking manner.
327
- This method starts the connection in a background task.
328
- """
329
- if self._running:
330
- logger.warning("Client is already running.")
331
- return
332
-
333
- self._running = True
334
- try:
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
- except RuntimeError:
337
- loop = asyncio.new_event_loop()
338
- asyncio.set_event_loop(loop)
339
-
340
- self._background_task = asyncio.create_task(self._connect_with_backoff())
341
-
342
- def stop(self) -> None:
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.10.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-time market data.
135
+ The `QuotesClient` enables you to connect to Wizzer's WebSocket server for realtime 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", # Options: "error", "info", "debug"
149
- max_message_size=10 * 1024 * 1024, # Optional: Set max message size (default 10MB)
150
- batch_size=20 # Optional: Max instruments per subscription batch (default 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 to be set)
154
+ # (Requires WZ__QUOTES_BASE_URL and WZ__TOKEN set)
155
155
  client = QuotesClient(log_level="info")
156
156
 
157
- # Method 3: Mixed approach (some params direct, some from env vars)
157
+ # Method 3: Mixed approach
158
158
  client = QuotesClient(
159
- base_url="wss://custom-websocket-url/quotes",
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
- This approach is similar to Zerodha's KiteTicker and is recommended for simple scripts:
168
+ Blocks your main thread, similar to Zerodhas KiteTicker:
172
169
 
173
170
  ```python
174
- # This will block and run until stopped
175
171
  client.connect()
176
172
  ```
177
173
 
178
- #### Non-Blocking Connection
174
+ #### NonBlocking Connection
179
175
 
180
- Use this for more complex applications where you need to perform other tasks:
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
- Set up callbacks to handle different events:
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
- """Called when a tick is received"""
197
- print(f"Received tick: {tick}")
190
+ print("Tick:", tick)
198
191
 
199
192
  def on_connect(ws):
200
- """Called when the connection is established"""
201
- print("Connected to the quotes server")
202
- # Subscribe to instruments
203
- ws.subscribe(["NSE:SBIN:3045"])
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
- """Called when the connection is closed"""
207
- print(f"Connection closed with code {code}: {reason}")
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
- """Called when an error occurs"""
213
- print(f"Error: {error}")
205
+ print("Error:", error)
214
206
 
215
- # Set the callbacks
216
- client.on_tick = on_tick
207
+ client.on_tick = on_tick
217
208
  client.on_connect = on_connect
218
- client.on_close = on_close
219
- client.on_error = 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
- Subscribe to receive market data for specific instruments:
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
- # Subscribe to a single instrument
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
- To stop receiving data for specific instruments:
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
- ### Handling WebSocket Connection
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
- #### Complete Blocking Example
237
+ #### Blocking Example
268
238
 
269
239
  ```python
270
240
  import logging
271
241
  from wiz_trader import QuotesClient
272
242
 
273
- # Configure logging
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
- """Process incoming market data"""
284
- logging.debug("Received tick: %s", tick)
251
+ logging.debug("Tick: %s", tick)
285
252
 
286
253
  def on_connect(ws):
287
- """Handle successful connection"""
288
- logging.info("Connected to quotes server")
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
- """Handle connection closure"""
294
- logging.info("Connection closed: %s", reason)
258
+ logging.warning("Closed: %s", reason)
259
+ ws.stop()
295
260
 
296
261
  def on_error(ws, error):
297
- """Handle errors"""
298
- logging.error("Error occurred: %s", error)
262
+ logging.error("Error: %s", error)
299
263
 
300
- # Set callbacks
301
- client.on_tick = on_tick
264
+ client.on_tick = on_tick
302
265
  client.on_connect = on_connect
303
- client.on_close = on_close
304
- client.on_error = 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-Blocking Example with Other Operations
275
+ #### NonBlocking Example
315
276
 
316
277
  ```python
317
278
  import asyncio
318
279
  import logging
319
- from wiz_trader import QuotesClient, WizzerClient
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
- # Initialize the REST API client
332
- wizzer_client = WizzerClient(
333
- base_url="https://api-url.in",
334
- token="your-jwt-token"
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
- if __name__ == "__main__":
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,,