wiz-trader 0.7.0__py3-none-any.whl → 0.9.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 +101 -25
- {wiz_trader-0.7.0.dist-info → wiz_trader-0.9.0.dist-info}/METADATA +1 -1
- wiz_trader-0.9.0.dist-info/RECORD +9 -0
- wiz_trader-0.7.0.dist-info/RECORD +0 -9
- {wiz_trader-0.7.0.dist-info → wiz_trader-0.9.0.dist-info}/WHEEL +0 -0
- {wiz_trader-0.7.0.dist-info → wiz_trader-0.9.0.dist-info}/top_level.txt +0 -0
wiz_trader/__init__.py
CHANGED
wiz_trader/quotes/client.py
CHANGED
@@ -3,7 +3,7 @@ import json
|
|
3
3
|
import os
|
4
4
|
import logging
|
5
5
|
import random
|
6
|
-
from typing import Callable, List, Optional
|
6
|
+
from typing import Callable, List, Optional, Any, Iterator
|
7
7
|
|
8
8
|
import websockets
|
9
9
|
from websockets.exceptions import ConnectionClosed
|
@@ -33,7 +33,9 @@ class QuotesClient:
|
|
33
33
|
self,
|
34
34
|
base_url: Optional[str] = None,
|
35
35
|
token: Optional[str] = None,
|
36
|
-
log_level: str = "error" # default only errors
|
36
|
+
log_level: str = "error", # default only errors
|
37
|
+
max_message_size: int = 10 * 1024 * 1024, # 10MB default max size
|
38
|
+
batch_size: int = 20 # Max number of instruments to subscribe to at once
|
37
39
|
):
|
38
40
|
# Configure logger based on log_level.
|
39
41
|
valid_levels = {"error": logging.ERROR, "info": logging.INFO, "debug": logging.DEBUG}
|
@@ -42,6 +44,8 @@ class QuotesClient:
|
|
42
44
|
logger.setLevel(valid_levels[log_level])
|
43
45
|
|
44
46
|
self.log_level = log_level
|
47
|
+
self.max_message_size = max_message_size
|
48
|
+
self.batch_size = batch_size
|
45
49
|
# System env vars take precedence over .env
|
46
50
|
self.base_url = base_url or os.environ.get("WZ__QUOTES_BASE_URL")
|
47
51
|
self.token = token or os.environ.get("WZ__TOKEN")
|
@@ -63,6 +67,20 @@ class QuotesClient:
|
|
63
67
|
|
64
68
|
logger.debug("Initialized QuotesClient with URL: %s", self.url)
|
65
69
|
|
70
|
+
def _chunk_list(self, data: List[Any], chunk_size: int) -> Iterator[List[Any]]:
|
71
|
+
"""
|
72
|
+
Split a list into smaller chunks.
|
73
|
+
|
74
|
+
Args:
|
75
|
+
data (List[Any]): The list to split.
|
76
|
+
chunk_size (int): Maximum size of each chunk.
|
77
|
+
|
78
|
+
Returns:
|
79
|
+
Iterator[List[Any]]: Iterator of list chunks.
|
80
|
+
"""
|
81
|
+
for i in range(0, len(data), chunk_size):
|
82
|
+
yield data[i:i + chunk_size]
|
83
|
+
|
66
84
|
async def connect(self) -> None:
|
67
85
|
"""
|
68
86
|
Continuously connect to the quotes server and process incoming messages.
|
@@ -73,18 +91,23 @@ class QuotesClient:
|
|
73
91
|
while True:
|
74
92
|
try:
|
75
93
|
logger.info("Connecting to %s ...", self.url)
|
76
|
-
async with websockets.connect(self.url) as websocket:
|
94
|
+
async with websockets.connect(self.url, max_size=self.max_message_size) as websocket:
|
77
95
|
self.ws = websocket
|
78
96
|
logger.info("Connected to the quotes server.")
|
79
97
|
|
80
98
|
# On reconnection, re-subscribe if needed.
|
81
99
|
if self.subscribed_instruments:
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
100
|
+
# Split into batches to avoid message size issues
|
101
|
+
instruments_list = list(self.subscribed_instruments)
|
102
|
+
for batch in self._chunk_list(instruments_list, self.batch_size):
|
103
|
+
subscribe_msg = {
|
104
|
+
"action": "subscribe",
|
105
|
+
"instruments": batch
|
106
|
+
}
|
107
|
+
await self.ws.send(json.dumps(subscribe_msg))
|
108
|
+
logger.info("Re-subscribed to batch of %d instruments", len(batch))
|
109
|
+
# Small delay between batches to avoid overwhelming the server
|
110
|
+
await asyncio.sleep(0.1)
|
88
111
|
|
89
112
|
# Reset backoff after a successful connection.
|
90
113
|
backoff = self._backoff_base
|
@@ -106,23 +129,53 @@ class QuotesClient:
|
|
106
129
|
async def _handle_messages(self) -> None:
|
107
130
|
"""
|
108
131
|
Handle incoming messages and dispatch them via the on_tick callback.
|
132
|
+
Handles newline-delimited JSON objects in a single message.
|
109
133
|
"""
|
110
134
|
try:
|
111
135
|
async for message in self.ws: # type: ignore
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
136
|
+
# Log message size for debugging large message issues
|
137
|
+
if self.log_level == "debug" and isinstance(message, str):
|
138
|
+
message_size = len(message.encode("utf-8"))
|
139
|
+
if message_size > 1024 * 1024: # Over 1MB
|
140
|
+
logger.debug("Received large message: %d bytes", message_size)
|
141
|
+
# Log the beginning of the message for debugging
|
142
|
+
logger.debug("Message starts with: %s", message[:100])
|
143
|
+
|
144
|
+
if isinstance(message, str):
|
145
|
+
# Special handling for newline-delimited JSON
|
146
|
+
if '\n' in message:
|
147
|
+
# Split by newlines and process each JSON object separately
|
148
|
+
for json_str in message.strip().split('\n'):
|
149
|
+
if not json_str:
|
150
|
+
continue
|
151
|
+
|
152
|
+
try:
|
153
|
+
tick = json.loads(json_str)
|
154
|
+
if self.on_tick:
|
155
|
+
self.on_tick(tick)
|
156
|
+
except json.JSONDecodeError as e:
|
157
|
+
logger.error("Failed to parse JSON object: %s", str(e))
|
158
|
+
logger.error("Invalid JSON: %s...", json_str[:100])
|
116
159
|
else:
|
117
|
-
|
118
|
-
|
119
|
-
|
160
|
+
# Single JSON object
|
161
|
+
try:
|
162
|
+
tick = json.loads(message)
|
163
|
+
if self.on_tick:
|
164
|
+
self.on_tick(tick)
|
165
|
+
except json.JSONDecodeError as e:
|
166
|
+
logger.error("Failed to parse JSON: %s", str(e))
|
167
|
+
logger.error("Invalid JSON message: %s...", message[:100])
|
168
|
+
else:
|
169
|
+
logger.warning("Received non-string message: %s", type(message))
|
120
170
|
except ConnectionClosed as e:
|
121
171
|
logger.info("Connection closed during message handling: %s", e)
|
172
|
+
except Exception as e:
|
173
|
+
logger.error("Error processing message: %s", str(e), exc_info=True)
|
122
174
|
|
123
175
|
async def subscribe(self, instruments: List[str]) -> None:
|
124
176
|
"""
|
125
177
|
Subscribe to a list of instruments and update the subscription list.
|
178
|
+
Splits large subscription requests into batches to avoid message size issues.
|
126
179
|
|
127
180
|
Args:
|
128
181
|
instruments (List[str]): List of instrument identifiers.
|
@@ -131,32 +184,55 @@ class QuotesClient:
|
|
131
184
|
new_instruments = set(instruments) - self.subscribed_instruments
|
132
185
|
if new_instruments:
|
133
186
|
self.subscribed_instruments.update(new_instruments)
|
134
|
-
|
135
|
-
|
136
|
-
|
187
|
+
|
188
|
+
# Split into batches to avoid message size issues
|
189
|
+
new_instruments_list = list(new_instruments)
|
190
|
+
for batch in self._chunk_list(new_instruments_list, self.batch_size):
|
191
|
+
logger.info("Subscribing to batch of %d instruments", len(batch))
|
192
|
+
message = {"action": "subscribe", "instruments": batch}
|
193
|
+
await self.ws.send(json.dumps(message))
|
194
|
+
# Small delay between batches to avoid overwhelming the server
|
195
|
+
await asyncio.sleep(0.1)
|
196
|
+
|
197
|
+
logger.info("Completed subscription for %d new instruments", len(new_instruments))
|
137
198
|
else:
|
138
199
|
logger.info("Instruments already subscribed: %s", instruments)
|
139
200
|
else:
|
140
201
|
logger.info("Cannot subscribe: WebSocket is not connected.")
|
202
|
+
# Still update the subscription list so we can subscribe when connected
|
203
|
+
self.subscribed_instruments.update(set(instruments))
|
141
204
|
|
142
205
|
async def unsubscribe(self, instruments: List[str]) -> None:
|
143
206
|
"""
|
144
207
|
Unsubscribe from a list of instruments and update the subscription list.
|
208
|
+
Splits large unsubscription requests into batches to avoid message size issues.
|
145
209
|
|
146
210
|
Args:
|
147
211
|
instruments (List[str]): List of instrument identifiers.
|
148
212
|
"""
|
149
213
|
if self.ws and self.ws.state == State.OPEN:
|
150
214
|
unsub_set = set(instruments)
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
215
|
+
to_unsubscribe = unsub_set & self.subscribed_instruments
|
216
|
+
|
217
|
+
if to_unsubscribe:
|
218
|
+
self.subscribed_instruments.difference_update(to_unsubscribe)
|
219
|
+
|
220
|
+
# Split into batches to avoid message size issues
|
221
|
+
unsub_list = list(to_unsubscribe)
|
222
|
+
for batch in self._chunk_list(unsub_list, self.batch_size):
|
223
|
+
logger.info("Unsubscribing from batch of %d instruments", len(batch))
|
224
|
+
message = {"action": "unsubscribe", "instruments": batch}
|
225
|
+
await self.ws.send(json.dumps(message))
|
226
|
+
# Small delay between batches to avoid overwhelming the server
|
227
|
+
await asyncio.sleep(0.1)
|
228
|
+
|
229
|
+
logger.info("Completed unsubscription for %d instruments", len(to_unsubscribe))
|
156
230
|
else:
|
157
231
|
logger.info("No matching instruments found in current subscription.")
|
158
232
|
else:
|
159
233
|
logger.info("Cannot unsubscribe: WebSocket is not connected.")
|
234
|
+
# Still update the subscription list
|
235
|
+
self.subscribed_instruments.difference_update(set(instruments))
|
160
236
|
|
161
237
|
async def close(self) -> None:
|
162
238
|
"""
|
@@ -164,4 +240,4 @@ class QuotesClient:
|
|
164
240
|
"""
|
165
241
|
if self.ws:
|
166
242
|
await self.ws.close()
|
167
|
-
logger.info("WebSocket connection closed.")
|
243
|
+
logger.info("WebSocket connection closed.")
|
@@ -0,0 +1,9 @@
|
|
1
|
+
wiz_trader/__init__.py,sha256=IOxpQuLhYFFBe75RCqoa81faDkt84n1pMIuiTND5_Kc,181
|
2
|
+
wiz_trader/apis/__init__.py,sha256=ItWKMOl4omiW0g2f-M7WRW3v-dss_ULd9vYnFyIIT9o,132
|
3
|
+
wiz_trader/apis/client.py,sha256=rq6FEA5DDCGXc4axSSzOdVvet1NWlW8L843GMp0qXQA,20562
|
4
|
+
wiz_trader/quotes/__init__.py,sha256=RF9g9CNP6bVWlmCh_ad8krm3-EWOIuVfLp0-H9fAeEM,108
|
5
|
+
wiz_trader/quotes/client.py,sha256=gYiX65AjzCEHIwCROjcu7d6uGirpGj4bnkNQ2UOQEv8,9790
|
6
|
+
wiz_trader-0.9.0.dist-info/METADATA,sha256=sfnuzoMTsXBeUYdnJ7-Bns7116N5VErpbiIsFfkkigU,4281
|
7
|
+
wiz_trader-0.9.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
8
|
+
wiz_trader-0.9.0.dist-info/top_level.txt,sha256=lnYS_g8LlA6ryKYnvY8xIQ6K2K-xzOsd-99AWgnW6VY,11
|
9
|
+
wiz_trader-0.9.0.dist-info/RECORD,,
|
@@ -1,9 +0,0 @@
|
|
1
|
-
wiz_trader/__init__.py,sha256=V4DgwVGNtcnqYZhVe7pww5-IHtYqge5CQXqkaS81mfo,181
|
2
|
-
wiz_trader/apis/__init__.py,sha256=ItWKMOl4omiW0g2f-M7WRW3v-dss_ULd9vYnFyIIT9o,132
|
3
|
-
wiz_trader/apis/client.py,sha256=rq6FEA5DDCGXc4axSSzOdVvet1NWlW8L843GMp0qXQA,20562
|
4
|
-
wiz_trader/quotes/__init__.py,sha256=RF9g9CNP6bVWlmCh_ad8krm3-EWOIuVfLp0-H9fAeEM,108
|
5
|
-
wiz_trader/quotes/client.py,sha256=fUHTMGDauGF9cjsFsVAzoOwqSgD555_TLoNqmnFhLdQ,6203
|
6
|
-
wiz_trader-0.7.0.dist-info/METADATA,sha256=kqDpwsUfiECcbxhVKPUEuSedAu50L-OOPX2M4JAW12o,4281
|
7
|
-
wiz_trader-0.7.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
8
|
-
wiz_trader-0.7.0.dist-info/top_level.txt,sha256=lnYS_g8LlA6ryKYnvY8xIQ6K2K-xzOsd-99AWgnW6VY,11
|
9
|
-
wiz_trader-0.7.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|