wiz-trader 0.27.0__py3-none-any.whl → 0.29.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 +90 -18
- {wiz_trader-0.27.0.dist-info → wiz_trader-0.29.0.dist-info}/METADATA +67 -12
- wiz_trader-0.29.0.dist-info/RECORD +9 -0
- wiz_trader-0.27.0.dist-info/RECORD +0 -9
- {wiz_trader-0.27.0.dist-info → wiz_trader-0.29.0.dist-info}/WHEEL +0 -0
- {wiz_trader-0.27.0.dist-info → wiz_trader-0.29.0.dist-info}/top_level.txt +0 -0
wiz_trader/__init__.py
CHANGED
wiz_trader/quotes/client.py
CHANGED
@@ -7,6 +7,7 @@ from typing import Callable, List, Optional, Any, Iterator
|
|
7
7
|
|
8
8
|
import websockets
|
9
9
|
from websockets.exceptions import ConnectionClosed
|
10
|
+
from websockets.protocol import State
|
10
11
|
|
11
12
|
# Setup module‐level logger with a default handler if none exists.
|
12
13
|
logger = logging.getLogger(__name__)
|
@@ -25,10 +26,26 @@ class QuotesClient:
|
|
25
26
|
base_url (str): WebSocket URL of the quotes server.
|
26
27
|
token (str): JWT token for authentication.
|
27
28
|
log_level (str): Logging level. Options: "error", "info", "debug".
|
29
|
+
on_tick (Callable): Callback for tick messages (type='ticks' or no type field).
|
30
|
+
on_stats (Callable): Callback for stats/greeks messages (type='greeks').
|
31
|
+
on_connect (Callable): Callback when connection is established.
|
32
|
+
on_close (Callable): Callback when connection is closed.
|
33
|
+
on_error (Callable): Callback for errors.
|
34
|
+
|
35
|
+
Message Routing:
|
36
|
+
- Messages with type='ticks' are routed to on_tick callback
|
37
|
+
- Messages with type='greeks' are routed to on_stats callback
|
38
|
+
- Messages without type field are routed to on_tick for backward compatibility
|
39
|
+
- Messages are silently dropped if the appropriate handler is not registered
|
28
40
|
"""
|
29
41
|
|
30
42
|
ACTION_SUBSCRIBE = "subscribe"
|
31
43
|
ACTION_UNSUBSCRIBE = "unsubscribe"
|
44
|
+
|
45
|
+
# Subscription modes
|
46
|
+
MODE_GREEKS = "greeks"
|
47
|
+
MODE_TICKS = "ticks"
|
48
|
+
MODE_FULL = "full"
|
32
49
|
|
33
50
|
def __init__(
|
34
51
|
self,
|
@@ -57,6 +74,7 @@ class QuotesClient:
|
|
57
74
|
self.url = f"{self.base_url}?token={self.token}"
|
58
75
|
self.ws: Optional[websockets.WebSocketClientProtocol] = None
|
59
76
|
self.subscribed_instruments: set = set()
|
77
|
+
self.subscription_modes: dict = {} # instrument -> mode mapping
|
60
78
|
self._running = False
|
61
79
|
self._background_task = None
|
62
80
|
self._loop: Optional[asyncio.AbstractEventLoop] = None
|
@@ -67,6 +85,7 @@ class QuotesClient:
|
|
67
85
|
|
68
86
|
# Callbacks are plain synchronous functions
|
69
87
|
self.on_tick: Optional[Callable[[Any, dict], None]] = None
|
88
|
+
self.on_stats: Optional[Callable[[Any, dict], None]] = None
|
70
89
|
self.on_connect: Optional[Callable[[Any], None]] = None
|
71
90
|
self.on_close: Optional[Callable[[Any, Optional[int], Optional[str]], None]] = None
|
72
91
|
self.on_error: Optional[Callable[[Any, Exception], None]] = None
|
@@ -94,13 +113,26 @@ class QuotesClient:
|
|
94
113
|
except Exception as e:
|
95
114
|
logger.error("Error in on_connect callback: %s", e, exc_info=True)
|
96
115
|
|
97
|
-
# re-subscribe on reconnect
|
116
|
+
# re-subscribe on reconnect with modes
|
98
117
|
if self.subscribed_instruments:
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
118
|
+
# Group instruments by mode for efficient re-subscription
|
119
|
+
mode_groups = {}
|
120
|
+
for instrument in self.subscribed_instruments:
|
121
|
+
mode = self.subscription_modes.get(instrument, self.MODE_TICKS)
|
122
|
+
if mode not in mode_groups:
|
123
|
+
mode_groups[mode] = []
|
124
|
+
mode_groups[mode].append(instrument)
|
125
|
+
|
126
|
+
for mode, instruments in mode_groups.items():
|
127
|
+
for batch in self._chunk_list(instruments, self.batch_size):
|
128
|
+
msg = {
|
129
|
+
"action": self.ACTION_SUBSCRIBE,
|
130
|
+
"instruments": batch,
|
131
|
+
"mode": mode
|
132
|
+
}
|
133
|
+
await self.ws.send(json.dumps(msg))
|
134
|
+
logger.info("Re-subscribed to %d instruments with mode '%s'", len(batch), mode)
|
135
|
+
await asyncio.sleep(0.1)
|
104
136
|
|
105
137
|
backoff = self._backoff_base
|
106
138
|
await self._handle_messages()
|
@@ -142,9 +174,26 @@ class QuotesClient:
|
|
142
174
|
if not chunk:
|
143
175
|
continue
|
144
176
|
try:
|
145
|
-
|
146
|
-
|
147
|
-
|
177
|
+
data = json.loads(chunk)
|
178
|
+
# Route based on message type
|
179
|
+
message_type = data.get('type')
|
180
|
+
|
181
|
+
if message_type == 'greeks':
|
182
|
+
if self.on_stats:
|
183
|
+
self.on_stats(self, data)
|
184
|
+
else:
|
185
|
+
logger.debug("Received greeks message but no on_stats handler registered")
|
186
|
+
elif message_type == 'ticks':
|
187
|
+
if self.on_tick:
|
188
|
+
self.on_tick(self, data)
|
189
|
+
else:
|
190
|
+
logger.debug("Received ticks message but no on_tick handler registered")
|
191
|
+
else:
|
192
|
+
# No type field - send to on_tick for backward compatibility
|
193
|
+
if self.on_tick:
|
194
|
+
self.on_tick(self, data)
|
195
|
+
else:
|
196
|
+
logger.debug("Received message without type field and no on_tick handler registered")
|
148
197
|
except json.JSONDecodeError as e:
|
149
198
|
logger.error("JSON parse error: %s", e)
|
150
199
|
else:
|
@@ -161,26 +210,40 @@ class QuotesClient:
|
|
161
210
|
|
162
211
|
# -- Async core methods (for internal use) --
|
163
212
|
|
164
|
-
async def _subscribe_async(self, instruments: List[str]) -> None:
|
165
|
-
if self.ws and self.ws.
|
213
|
+
async def _subscribe_async(self, instruments: List[str], mode: str = MODE_TICKS) -> None:
|
214
|
+
if self.ws and self.ws.state == State.OPEN:
|
166
215
|
new = set(instruments) - self.subscribed_instruments
|
167
216
|
if new:
|
168
217
|
self.subscribed_instruments |= new
|
218
|
+
# Track mode for each instrument
|
219
|
+
for instrument in new:
|
220
|
+
self.subscription_modes[instrument] = mode
|
221
|
+
|
169
222
|
for batch in self._chunk_list(list(new), self.batch_size):
|
170
|
-
logger.info("Subscribing to %d instruments", len(batch))
|
171
|
-
|
223
|
+
logger.info("Subscribing to %d instruments with mode '%s'", len(batch), mode)
|
224
|
+
message = {
|
172
225
|
"action": self.ACTION_SUBSCRIBE,
|
173
|
-
"instruments": batch
|
174
|
-
|
226
|
+
"instruments": batch,
|
227
|
+
"mode": mode
|
228
|
+
}
|
229
|
+
print(f"Subscribing to {batch} with mode {mode}")
|
230
|
+
await self.ws.send(json.dumps(message))
|
175
231
|
await asyncio.sleep(0.1)
|
176
232
|
else:
|
177
233
|
self.subscribed_instruments |= set(instruments)
|
234
|
+
# Track mode for each instrument
|
235
|
+
for instrument in instruments:
|
236
|
+
self.subscription_modes[instrument] = mode
|
178
237
|
|
179
238
|
async def _unsubscribe_async(self, instruments: List[str]) -> None:
|
180
|
-
if self.ws and self.ws.
|
239
|
+
if self.ws and self.ws.state == State.OPEN:
|
181
240
|
to_remove = set(instruments) & self.subscribed_instruments
|
182
241
|
if to_remove:
|
183
242
|
self.subscribed_instruments -= to_remove
|
243
|
+
# Remove mode tracking for unsubscribed instruments
|
244
|
+
for instrument in to_remove:
|
245
|
+
self.subscription_modes.pop(instrument, None)
|
246
|
+
|
184
247
|
for batch in self._chunk_list(list(to_remove), self.batch_size):
|
185
248
|
logger.info("Unsubscribing from %d instruments", len(batch))
|
186
249
|
await self.ws.send(json.dumps({
|
@@ -190,20 +253,26 @@ class QuotesClient:
|
|
190
253
|
await asyncio.sleep(0.1)
|
191
254
|
else:
|
192
255
|
self.subscribed_instruments -= set(instruments)
|
256
|
+
# Remove mode tracking for unsubscribed instruments
|
257
|
+
for instrument in instruments:
|
258
|
+
self.subscription_modes.pop(instrument, None)
|
193
259
|
|
194
260
|
# -- Public wrappers for plain callback users --
|
195
261
|
|
196
|
-
def subscribe(self, instruments: List[str]) -> None:
|
262
|
+
def subscribe(self, instruments: List[str], mode: str = MODE_TICKS) -> None:
|
197
263
|
"""
|
198
264
|
Schedule subscribe onto the client’s event loop.
|
199
265
|
"""
|
200
266
|
if self._loop:
|
201
267
|
asyncio.run_coroutine_threadsafe(
|
202
|
-
self._subscribe_async(instruments),
|
268
|
+
self._subscribe_async(instruments, mode),
|
203
269
|
self._loop
|
204
270
|
)
|
205
271
|
else:
|
206
272
|
self.subscribed_instruments |= set(instruments)
|
273
|
+
# Track mode for each instrument
|
274
|
+
for instrument in instruments:
|
275
|
+
self.subscription_modes[instrument] = mode
|
207
276
|
|
208
277
|
def unsubscribe(self, instruments: List[str]) -> None:
|
209
278
|
"""
|
@@ -216,6 +285,9 @@ class QuotesClient:
|
|
216
285
|
)
|
217
286
|
else:
|
218
287
|
self.subscribed_instruments -= set(instruments)
|
288
|
+
# Remove mode tracking for unsubscribed instruments
|
289
|
+
for instrument in instruments:
|
290
|
+
self.subscription_modes.pop(instrument, None)
|
219
291
|
|
220
292
|
def unsubscribe_all(self) -> None:
|
221
293
|
"""
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: wiz_trader
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.29.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
|
@@ -186,17 +186,25 @@ client.stop()
|
|
186
186
|
|
187
187
|
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.
|
188
188
|
|
189
|
+
The QuotesClient routes messages based on their `type` field:
|
190
|
+
- Messages with `type: 'ticks'` are sent to the `on_tick` callback
|
191
|
+
- Messages with `type: 'greeks'` are sent to the `on_stats` callback
|
192
|
+
- Messages without a `type` field are sent to `on_tick` for backward compatibility
|
193
|
+
|
189
194
|
```python
|
190
195
|
def on_tick(ws, tick):
|
191
196
|
print("Tick:", tick)
|
192
197
|
|
198
|
+
def on_stats(ws, stats):
|
199
|
+
print("Stats:", stats)
|
200
|
+
# Stats/Greek messages contain: {'type': 'greeks', 'identifier': '...', 'rho': 0.1, 'theta': 9.8, ...}
|
201
|
+
|
193
202
|
def on_connect(ws):
|
194
203
|
print("Connected!")
|
195
|
-
#
|
196
|
-
ws.subscribe([
|
197
|
-
|
198
|
-
|
199
|
-
])
|
204
|
+
# Subscribe with different modes
|
205
|
+
ws.subscribe(["NSE:SBIN:3045"], mode="ticks") # Will receive tick messages
|
206
|
+
ws.subscribe(["NSE:NIFTY25JULFUT:53216"], mode="greeks") # Will receive greek messages
|
207
|
+
ws.subscribe(["NSE:RELIANCE:2885"], mode="full") # Will receive both types
|
200
208
|
|
201
209
|
def on_close(ws, code, reason):
|
202
210
|
print(f"Connection closed [{code}]: {reason}")
|
@@ -206,6 +214,7 @@ def on_error(ws, error):
|
|
206
214
|
print("Error:", error)
|
207
215
|
|
208
216
|
client.on_tick = on_tick
|
217
|
+
client.on_stats = on_stats
|
209
218
|
client.on_connect = on_connect
|
210
219
|
client.on_close = on_close
|
211
220
|
client.on_error = on_error
|
@@ -261,9 +270,13 @@ client = QuotesClient(
|
|
261
270
|
def on_tick(ws, tick):
|
262
271
|
logging.debug("Tick: %s", tick)
|
263
272
|
|
273
|
+
def on_stats(ws, stats):
|
274
|
+
logging.debug("Stats: %s", stats)
|
275
|
+
|
264
276
|
def on_connect(ws):
|
265
277
|
logging.info("Connected.")
|
266
|
-
ws.subscribe(["NSE:SBIN:3045", "
|
278
|
+
ws.subscribe(["NSE:SBIN:3045"], mode="ticks")
|
279
|
+
ws.subscribe(["NSE:NIFTY24JUN20100CE:20100"], mode="greeks")
|
267
280
|
|
268
281
|
def on_close(ws, code, reason):
|
269
282
|
logging.warning("Closed: %s", reason)
|
@@ -273,6 +286,7 @@ def on_error(ws, error):
|
|
273
286
|
logging.error("Error: %s", error)
|
274
287
|
|
275
288
|
client.on_tick = on_tick
|
289
|
+
client.on_stats = on_stats
|
276
290
|
client.on_connect = on_connect
|
277
291
|
client.on_close = on_close
|
278
292
|
client.on_error = on_error
|
@@ -307,17 +321,58 @@ async def main():
|
|
307
321
|
asyncio.run(main())
|
308
322
|
```
|
309
323
|
|
310
|
-
####
|
311
|
-
ts = tick timestamp
|
312
|
-
lastTradedTs = timestamp of the last executed trade in this instrument
|
324
|
+
#### Message Data Structures
|
313
325
|
|
314
|
-
|
326
|
+
##### Ticks structure
|
327
|
+
Messages with `type: 'ticks'` contain market data:
|
328
|
+
- `ts`: tick timestamp
|
329
|
+
- `lastTradedTs`: timestamp of the last executed trade in this instrument
|
315
330
|
- `oi`: Current open interest (for futures and options)
|
316
331
|
- `oiDayHigh`: Day's highest open interest value
|
317
332
|
- `oiDayLow`: Day's lowest open interest value
|
318
333
|
|
334
|
+
```json
|
335
|
+
{
|
336
|
+
"type": "ticks",
|
337
|
+
"identifier": "NSE:RELIANCE:2885",
|
338
|
+
"tradingSymbol": "RELIANCE",
|
339
|
+
"exchange": "NSE",
|
340
|
+
"segment": "NSECM",
|
341
|
+
"exchangeToken": 2885,
|
342
|
+
"bids": [{"volume": 1722, "price": 1295.5, "orders": 43}, ...],
|
343
|
+
"offers": [{"volume": 0, "price": 0, "orders": 0}, ...],
|
344
|
+
"ltp": 1295.5,
|
345
|
+
"lastTradedQty": 10,
|
346
|
+
"buyQty": 1722,
|
347
|
+
"sellQty": 0,
|
348
|
+
"volume": 10429964,
|
349
|
+
"avgPrice": 1291.46,
|
350
|
+
"netChange": 0,
|
351
|
+
"ohlc": {"open": 1270, "high": 1300.9, "low": 1267, "close": 1295.5},
|
352
|
+
"oi": 1234567,
|
353
|
+
"oiDayHigh": 1250000,
|
354
|
+
"oiDayLow": 1200000,
|
355
|
+
"lastTradedTs": "2025-04-21T10:29:33Z",
|
356
|
+
"ts": "2025-04-21T10:29:46Z"
|
357
|
+
}
|
319
358
|
```
|
320
|
-
|
359
|
+
|
360
|
+
##### Greeks structure
|
361
|
+
Messages with `type: 'greeks'` contain options Greeks data:
|
362
|
+
|
363
|
+
```json
|
364
|
+
{
|
365
|
+
"type": "greeks",
|
366
|
+
"identifier": "NSE:NIFTY25JULFUT:53216",
|
367
|
+
"tradingSymbol": "NIFTY25JULFUT",
|
368
|
+
"iv": 0.0,
|
369
|
+
"delta": 0.0,
|
370
|
+
"gamma": 0.0,
|
371
|
+
"theta": 0.0,
|
372
|
+
"vega": 0.0,
|
373
|
+
"rho": 0.0,
|
374
|
+
"ts": "2025-04-21T10:29:46Z"
|
375
|
+
}
|
321
376
|
```
|
322
377
|
|
323
378
|
## Wizzer Client
|
@@ -0,0 +1,9 @@
|
|
1
|
+
wiz_trader/__init__.py,sha256=10s3-fzxys3kNbg_710zCCN9V8iSlSgQuYHpXt6bBj4,183
|
2
|
+
wiz_trader/apis/__init__.py,sha256=6sUr1nzmplNdld0zryMrQSt0jHT2GhOiFYgKKVHzk8U,133
|
3
|
+
wiz_trader/apis/client.py,sha256=VEotcYfPkjmpp2seJtTWyEdppa6q9NecX89iWxnFQf0,63154
|
4
|
+
wiz_trader/quotes/__init__.py,sha256=RF9g9CNP6bVWlmCh_ad8krm3-EWOIuVfLp0-H9fAeEM,108
|
5
|
+
wiz_trader/quotes/client.py,sha256=aZ5LVlrj0mKfgHgFxERmk2HDZraB6RMaormTOMlqWZc,14915
|
6
|
+
wiz_trader-0.29.0.dist-info/METADATA,sha256=VoItlRQN9455ZqKUYbt96w_yxNih39jcZlFQIwdMUxY,99294
|
7
|
+
wiz_trader-0.29.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
8
|
+
wiz_trader-0.29.0.dist-info/top_level.txt,sha256=lnYS_g8LlA6ryKYnvY8xIQ6K2K-xzOsd-99AWgnW6VY,11
|
9
|
+
wiz_trader-0.29.0.dist-info/RECORD,,
|
@@ -1,9 +0,0 @@
|
|
1
|
-
wiz_trader/__init__.py,sha256=UugQriOgr3WKD8FxmXN8c7SfcZiCRJjptS9X63oF3tg,183
|
2
|
-
wiz_trader/apis/__init__.py,sha256=6sUr1nzmplNdld0zryMrQSt0jHT2GhOiFYgKKVHzk8U,133
|
3
|
-
wiz_trader/apis/client.py,sha256=VEotcYfPkjmpp2seJtTWyEdppa6q9NecX89iWxnFQf0,63154
|
4
|
-
wiz_trader/quotes/__init__.py,sha256=RF9g9CNP6bVWlmCh_ad8krm3-EWOIuVfLp0-H9fAeEM,108
|
5
|
-
wiz_trader/quotes/client.py,sha256=EkRxCudEq0J6ReP5nBgDzrL1XbtgWhrzpZT27xXBMBU,10891
|
6
|
-
wiz_trader-0.27.0.dist-info/METADATA,sha256=-At9ukmtGQGYHXuiDM5Yi0x-DjVXvlzNzQYJmnOt7nk,98383
|
7
|
-
wiz_trader-0.27.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
8
|
-
wiz_trader-0.27.0.dist-info/top_level.txt,sha256=lnYS_g8LlA6ryKYnvY8xIQ6K2K-xzOsd-99AWgnW6VY,11
|
9
|
-
wiz_trader-0.27.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|