wiz-trader 0.21.0__py3-none-any.whl → 0.23.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 +97 -73
- {wiz_trader-0.21.0.dist-info → wiz_trader-0.23.0.dist-info}/METADATA +1 -1
- wiz_trader-0.23.0.dist-info/RECORD +9 -0
- wiz_trader-0.21.0.dist-info/RECORD +0 -9
- {wiz_trader-0.21.0.dist-info → wiz_trader-0.23.0.dist-info}/WHEEL +0 -0
- {wiz_trader-0.21.0.dist-info → wiz_trader-0.23.0.dist-info}/top_level.txt +0 -0
wiz_trader/__init__.py
CHANGED
wiz_trader/quotes/client.py
CHANGED
@@ -3,11 +3,16 @@ import json
|
|
3
3
|
import os
|
4
4
|
import logging
|
5
5
|
import random
|
6
|
-
from typing import Callable, List, Optional, Any, Iterator
|
6
|
+
from typing import Callable, List, Optional, Any, Iterator, Dict, Set, TypeVar, Protocol, runtime_checkable
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
T = TypeVar('T')
|
9
|
+
|
10
|
+
@runtime_checkable
|
11
|
+
class WebSocketClientProtocol(Protocol):
|
12
|
+
async def send(self, message: str) -> None: ...
|
13
|
+
async def close(self) -> None: ...
|
14
|
+
@property
|
15
|
+
def open(self) -> bool: ...
|
11
16
|
|
12
17
|
# Setup module‐level logger with a default handler if none exists.
|
13
18
|
logger = logging.getLogger(__name__)
|
@@ -39,6 +44,9 @@ class QuotesClient:
|
|
39
44
|
max_message_size: int = 10 * 1024 * 1024,
|
40
45
|
batch_size: int = 20
|
41
46
|
):
|
47
|
+
logger.debug("Initializing QuotesClient with params: base_url=%s, log_level=%s, max_message_size=%d, batch_size=%d",
|
48
|
+
base_url, log_level, max_message_size, batch_size)
|
49
|
+
|
42
50
|
valid_levels = {"error": logging.ERROR, "info": logging.INFO, "debug": logging.DEBUG}
|
43
51
|
if log_level not in valid_levels:
|
44
52
|
raise ValueError(f"log_level must be one of {list(valid_levels.keys())}")
|
@@ -56,65 +64,60 @@ class QuotesClient:
|
|
56
64
|
raise ValueError("Base URL must be provided as an argument or in .env (WZ__QUOTES_BASE_URL)")
|
57
65
|
|
58
66
|
self.url = f"{self.base_url}?token={self.token}"
|
59
|
-
self.ws: Optional[
|
60
|
-
self.subscribed_instruments:
|
67
|
+
self.ws: Optional[WebSocketClientProtocol] = None
|
68
|
+
self.subscribed_instruments: Set[str] = set()
|
61
69
|
self._running = False
|
62
|
-
self._background_task = None
|
70
|
+
self._background_task: Optional[asyncio.Task[None]] = None
|
63
71
|
|
64
72
|
self._backoff_base = 1
|
65
73
|
self._backoff_factor = 2
|
66
74
|
self._backoff_max = 60
|
67
75
|
|
68
76
|
# Callbacks are plain synchronous functions
|
69
|
-
self.on_tick: Optional[Callable[[Any,
|
77
|
+
self.on_tick: Optional[Callable[[Any, Dict[str, Any]], None]] = None
|
70
78
|
self.on_connect: Optional[Callable[[Any], None]] = None
|
71
79
|
self.on_close: Optional[Callable[[Any, Optional[int], Optional[str]], None]] = None
|
72
80
|
self.on_error: Optional[Callable[[Any, Exception], None]] = None
|
73
81
|
|
74
|
-
logger.debug("
|
82
|
+
logger.debug("QuotesClient initialized successfully")
|
75
83
|
|
76
84
|
def _chunk_list(self, data: List[Any], chunk_size: int) -> Iterator[List[Any]]:
|
77
|
-
|
78
|
-
|
85
|
+
logger.debug("Chunking list of size %d with chunk_size %d", len(data), chunk_size)
|
86
|
+
return (data[i:i + chunk_size] for i in range(0, len(data), chunk_size))
|
79
87
|
|
80
88
|
async def _connect_with_backoff(self) -> None:
|
81
89
|
backoff = self._backoff_base
|
90
|
+
logger.debug("Starting connection with initial backoff: %d", backoff)
|
82
91
|
|
83
92
|
while self._running:
|
84
93
|
try:
|
85
|
-
logger.
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
94
|
+
logger.debug("Attempting WebSocket connection to %s", self.url)
|
95
|
+
# Using string literal for import to avoid type checking issues
|
96
|
+
ws = await __import__('websockets').connect(self.url, max_size=self.max_message_size)
|
97
|
+
self.ws = ws
|
98
|
+
logger.debug("WebSocket connection established successfully")
|
99
|
+
|
100
|
+
if self.on_connect:
|
101
|
+
logger.debug("Executing on_connect callback")
|
102
|
+
try:
|
103
|
+
self.on_connect(self)
|
104
|
+
except Exception as e:
|
105
|
+
logger.error("Error in on_connect callback: %s", e, exc_info=True)
|
106
|
+
|
107
|
+
if self.subscribed_instruments:
|
108
|
+
logger.debug("Re-subscribing to %d instruments", len(self.subscribed_instruments))
|
109
|
+
for batch in self._chunk_list(list(self.subscribed_instruments), self.batch_size):
|
110
|
+
msg = {"action": self.ACTION_SUBSCRIBE, "instruments": batch}
|
111
|
+
if self.ws: # Check if ws is not None
|
101
112
|
await self.ws.send(json.dumps(msg))
|
102
113
|
logger.info("Re-subscribed to %d instruments", len(batch))
|
103
114
|
await asyncio.sleep(0.1)
|
104
115
|
|
105
|
-
|
106
|
-
|
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)
|
116
|
+
backoff = self._backoff_base
|
117
|
+
await self._handle_messages()
|
115
118
|
|
116
119
|
except Exception as e:
|
117
|
-
logger.
|
120
|
+
logger.debug("Connection error occurred: %s", str(e), exc_info=True)
|
118
121
|
if self.on_error:
|
119
122
|
try:
|
120
123
|
self.on_error(self, e)
|
@@ -122,70 +125,90 @@ class QuotesClient:
|
|
122
125
|
logger.error("Error in on_error callback: %s", ex, exc_info=True)
|
123
126
|
|
124
127
|
if not self._running:
|
128
|
+
logger.debug("Client stopped, breaking reconnection loop")
|
125
129
|
break
|
126
130
|
|
127
131
|
sleep_time = min(backoff, self._backoff_max)
|
128
|
-
logger.
|
132
|
+
logger.debug("Calculated reconnection backoff: %s seconds", sleep_time)
|
129
133
|
await asyncio.sleep(sleep_time)
|
130
134
|
backoff = backoff * self._backoff_factor + random.uniform(0, 1)
|
131
135
|
|
132
136
|
async def _handle_messages(self) -> None:
|
133
137
|
try:
|
138
|
+
logger.debug("Starting message handling loop")
|
139
|
+
if self.ws is None:
|
140
|
+
logger.error("WebSocket connection is None")
|
141
|
+
return
|
142
|
+
|
143
|
+
# Using string literal for import to avoid type checking issues
|
134
144
|
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
145
|
if isinstance(message, str):
|
146
|
+
msg_size = len(message.encode("utf-8"))
|
147
|
+
logger.debug("Received message of size: %d bytes", msg_size)
|
148
|
+
|
141
149
|
for chunk in message.strip().split("\n"):
|
142
150
|
if not chunk:
|
143
151
|
continue
|
144
152
|
try:
|
145
153
|
tick = json.loads(chunk)
|
154
|
+
logger.debug("Successfully parsed JSON message for instrument: %s",
|
155
|
+
tick.get('instrument', 'unknown'))
|
146
156
|
if self.on_tick:
|
157
|
+
logger.debug("Executing on_tick callback")
|
147
158
|
self.on_tick(self, tick)
|
148
159
|
except json.JSONDecodeError as e:
|
160
|
+
logger.debug("Failed to parse JSON message: %s. Content: %s", str(e), chunk[:100])
|
149
161
|
logger.error("JSON parse error: %s", e)
|
150
162
|
else:
|
151
|
-
logger.
|
152
|
-
|
153
|
-
logger.info("Connection closed during message handling")
|
163
|
+
logger.debug("Received non-string message of type: %s", type(message).__name__)
|
164
|
+
logger.warning("Non-string message: %s", type(message).__name__)
|
154
165
|
except Exception as e:
|
155
|
-
logger.
|
166
|
+
logger.debug("Error in message handling: %s", str(e), exc_info=True)
|
156
167
|
if self.on_error:
|
157
168
|
try:
|
158
169
|
self.on_error(self, e)
|
159
170
|
except Exception:
|
160
171
|
pass
|
161
172
|
|
162
|
-
# -- Async core methods (for internal use) --
|
163
|
-
|
164
173
|
async def _subscribe_async(self, instruments: List[str]) -> None:
|
165
|
-
""
|
166
|
-
|
167
|
-
"""
|
168
|
-
if self.ws and self.ws.state == State.OPEN:
|
174
|
+
logger.debug("Processing async subscription request for %d instruments", len(instruments))
|
175
|
+
if self.ws and self.ws.open:
|
169
176
|
new = set(instruments) - self.subscribed_instruments
|
170
177
|
if new:
|
178
|
+
logger.debug("Found %d new instruments to subscribe", len(new))
|
171
179
|
self.subscribed_instruments |= new
|
172
180
|
for batch in self._chunk_list(list(new), self.batch_size):
|
173
|
-
logger.
|
181
|
+
logger.debug("Sending subscription request for batch of %d instruments", len(batch))
|
174
182
|
await self.ws.send(json.dumps({
|
175
183
|
"action": self.ACTION_SUBSCRIBE,
|
176
184
|
"instruments": batch
|
177
185
|
}))
|
178
186
|
await asyncio.sleep(0.1)
|
179
187
|
else:
|
180
|
-
|
188
|
+
logger.debug("WebSocket not ready, queueing %d instruments for later subscription", len(instruments))
|
181
189
|
self.subscribed_instruments |= set(instruments)
|
182
190
|
|
183
|
-
|
191
|
+
async def _unsubscribe_async(self, instruments: List[str]) -> None:
|
192
|
+
logger.debug("Processing async unsubscription request for %d instruments", len(instruments))
|
193
|
+
if self.ws and self.ws.open:
|
194
|
+
to_remove = set(instruments) & self.subscribed_instruments
|
195
|
+
if to_remove:
|
196
|
+
logger.debug("Found %d instruments to unsubscribe", len(to_remove))
|
197
|
+
for batch in self._chunk_list(list(to_remove), self.batch_size):
|
198
|
+
logger.debug("Sending unsubscription request for batch of %d instruments", len(batch))
|
199
|
+
await self.ws.send(json.dumps({
|
200
|
+
"action": self.ACTION_UNSUBSCRIBE,
|
201
|
+
"instruments": batch
|
202
|
+
}))
|
203
|
+
await asyncio.sleep(0.1)
|
204
|
+
logger.debug("Removed %d instruments from subscription set", len(to_remove))
|
205
|
+
self.subscribed_instruments -= to_remove
|
206
|
+
else:
|
207
|
+
logger.debug("WebSocket not ready, removing %d instruments from queue", len(instruments))
|
208
|
+
self.subscribed_instruments -= set(instruments)
|
184
209
|
|
185
210
|
def subscribe(self, instruments: List[str]) -> None:
|
186
|
-
""
|
187
|
-
Schedule an async subscribe so users can call this without 'await'.
|
188
|
-
"""
|
211
|
+
logger.debug("Scheduling subscription for %d instruments", len(instruments))
|
189
212
|
try:
|
190
213
|
loop = asyncio.get_event_loop()
|
191
214
|
except RuntimeError:
|
@@ -193,17 +216,24 @@ class QuotesClient:
|
|
193
216
|
asyncio.set_event_loop(loop)
|
194
217
|
loop.create_task(self._subscribe_async(instruments))
|
195
218
|
|
196
|
-
|
219
|
+
def unsubscribe(self, instruments: List[str]) -> None:
|
220
|
+
logger.debug("Scheduling unsubscription for %d instruments", len(instruments))
|
221
|
+
try:
|
222
|
+
loop = asyncio.get_event_loop()
|
223
|
+
except RuntimeError:
|
224
|
+
loop = asyncio.new_event_loop()
|
225
|
+
asyncio.set_event_loop(loop)
|
226
|
+
loop.create_task(self._unsubscribe_async(instruments))
|
197
227
|
|
198
228
|
async def close(self) -> None:
|
199
|
-
""
|
200
|
-
Close the WebSocket connection.
|
201
|
-
"""
|
229
|
+
logger.debug("Initiating WebSocket connection closure")
|
202
230
|
self._running = False
|
203
231
|
if self.ws:
|
232
|
+
logger.debug("Closing active WebSocket connection")
|
204
233
|
await self.ws.close()
|
205
234
|
logger.info("WebSocket closed.")
|
206
235
|
if self._background_task and not self._background_task.done():
|
236
|
+
logger.debug("Cancelling background connection task")
|
207
237
|
self._background_task.cancel()
|
208
238
|
try:
|
209
239
|
await self._background_task
|
@@ -211,9 +241,7 @@ class QuotesClient:
|
|
211
241
|
pass
|
212
242
|
|
213
243
|
def connect(self) -> None:
|
214
|
-
""
|
215
|
-
Blocking connect (runs the internal asyncio loop until stop()).
|
216
|
-
"""
|
244
|
+
logger.debug("Starting blocking connect operation")
|
217
245
|
self._running = True
|
218
246
|
try:
|
219
247
|
loop = asyncio.get_event_loop()
|
@@ -234,9 +262,7 @@ class QuotesClient:
|
|
234
262
|
pass
|
235
263
|
|
236
264
|
def connect_async(self) -> None:
|
237
|
-
""
|
238
|
-
Non-blocking connect: starts the background task.
|
239
|
-
"""
|
265
|
+
logger.debug("Starting non-blocking async connect operation")
|
240
266
|
if self._running:
|
241
267
|
logger.warning("Client already running.")
|
242
268
|
return
|
@@ -245,8 +271,6 @@ class QuotesClient:
|
|
245
271
|
self._background_task = loop.create_task(self._connect_with_backoff())
|
246
272
|
|
247
273
|
def stop(self) -> None:
|
248
|
-
""
|
249
|
-
Signal the client to stop and close.
|
250
|
-
"""
|
274
|
+
logger.debug("Stop signal received")
|
251
275
|
self._running = False
|
252
276
|
logger.info("Client stopping; will close soon.")
|
@@ -0,0 +1,9 @@
|
|
1
|
+
wiz_trader/__init__.py,sha256=6c10AeBkvODzgW6IRSOimyIKsojSQTsjDk05-tZwcrs,182
|
2
|
+
wiz_trader/apis/__init__.py,sha256=ItWKMOl4omiW0g2f-M7WRW3v-dss_ULd9vYnFyIIT9o,132
|
3
|
+
wiz_trader/apis/client.py,sha256=GY1aAaV4ia1tnFnB2qaNqnv-qeUvkVlvw9xOKN54qIs,59786
|
4
|
+
wiz_trader/quotes/__init__.py,sha256=RF9g9CNP6bVWlmCh_ad8krm3-EWOIuVfLp0-H9fAeEM,108
|
5
|
+
wiz_trader/quotes/client.py,sha256=UTkBGwrrlNO6i5r8oH63B9vXTvkvb-_bFlqHGiHufik,12289
|
6
|
+
wiz_trader-0.23.0.dist-info/METADATA,sha256=O_3IvT5NG0r-YMthKIZZSq_refcgZRdZfT1LykSFwl0,87046
|
7
|
+
wiz_trader-0.23.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
8
|
+
wiz_trader-0.23.0.dist-info/top_level.txt,sha256=lnYS_g8LlA6ryKYnvY8xIQ6K2K-xzOsd-99AWgnW6VY,11
|
9
|
+
wiz_trader-0.23.0.dist-info/RECORD,,
|
@@ -1,9 +0,0 @@
|
|
1
|
-
wiz_trader/__init__.py,sha256=34Jeuq5zT3Iu6yzfCqm63Zjemvdt3p4Knbk642fZPyI,182
|
2
|
-
wiz_trader/apis/__init__.py,sha256=ItWKMOl4omiW0g2f-M7WRW3v-dss_ULd9vYnFyIIT9o,132
|
3
|
-
wiz_trader/apis/client.py,sha256=GY1aAaV4ia1tnFnB2qaNqnv-qeUvkVlvw9xOKN54qIs,59786
|
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.21.0.dist-info/METADATA,sha256=QAED1SHUWcA_U1IzP0_ugjC1Ne1xkrPsTPKJJIKyZkw,87046
|
7
|
-
wiz_trader-0.21.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
8
|
-
wiz_trader-0.21.0.dist-info/top_level.txt,sha256=lnYS_g8LlA6ryKYnvY8xIQ6K2K-xzOsd-99AWgnW6VY,11
|
9
|
-
wiz_trader-0.21.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|