wiz-trader 0.1.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.
- apis/__init__.py +3 -0
- apis/client.py +2 -0
- ticker/__init__.py +5 -0
- ticker/client.py +170 -0
- wiz_trader-0.1.0.dist-info/METADATA +43 -0
- wiz_trader-0.1.0.dist-info/RECORD +8 -0
- wiz_trader-0.1.0.dist-info/WHEEL +5 -0
- wiz_trader-0.1.0.dist-info/top_level.txt +2 -0
apis/__init__.py
ADDED
apis/client.py
ADDED
ticker/__init__.py
ADDED
ticker/client.py
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
import asyncio
|
2
|
+
import json
|
3
|
+
import os
|
4
|
+
import logging
|
5
|
+
import random
|
6
|
+
from typing import Callable, List, Optional
|
7
|
+
|
8
|
+
import websockets
|
9
|
+
from websockets.exceptions import ConnectionClosed
|
10
|
+
from websockets.protocol import State
|
11
|
+
from dotenv import load_dotenv
|
12
|
+
|
13
|
+
# Load environment variables from .env (if available)
|
14
|
+
load_dotenv()
|
15
|
+
|
16
|
+
# Setup module-level logger with a default handler if none exists.
|
17
|
+
logger = logging.getLogger(__name__)
|
18
|
+
if not logger.handlers:
|
19
|
+
handler = logging.StreamHandler()
|
20
|
+
formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
|
21
|
+
handler.setFormatter(formatter)
|
22
|
+
logger.addHandler(handler)
|
23
|
+
|
24
|
+
|
25
|
+
class QuotesClient:
|
26
|
+
"""
|
27
|
+
A Python SDK for connecting to the Quotes Server via WebSocket.
|
28
|
+
|
29
|
+
Attributes:
|
30
|
+
base_url (str): WebSocket URL of the quotes server.
|
31
|
+
token (str): JWT token for authentication.
|
32
|
+
on_tick (Callable[[dict], None]): Callback to process received tick data.
|
33
|
+
log_level (str): Logging level. Options: "error", "info", "debug".
|
34
|
+
"""
|
35
|
+
|
36
|
+
def __init__(
|
37
|
+
self,
|
38
|
+
base_url: Optional[str] = None,
|
39
|
+
token: Optional[str] = None,
|
40
|
+
log_level: str = "error" # default only errors
|
41
|
+
):
|
42
|
+
# Configure logger based on log_level.
|
43
|
+
valid_levels = {"error": logging.ERROR, "info": logging.INFO, "debug": logging.DEBUG}
|
44
|
+
if log_level not in valid_levels:
|
45
|
+
raise ValueError(f"log_level must be one of {list(valid_levels.keys())}")
|
46
|
+
logger.setLevel(valid_levels[log_level])
|
47
|
+
|
48
|
+
self.log_level = log_level
|
49
|
+
self.base_url = base_url or os.getenv("WZ__QUOTES_BASE_URL")
|
50
|
+
self.token = token or os.getenv("WZ__TOKEN")
|
51
|
+
if not self.token:
|
52
|
+
raise ValueError("JWT token must be provided as an argument or in .env (WZ__TOKEN)")
|
53
|
+
if not self.base_url:
|
54
|
+
raise ValueError("Base URL must be provided as an argument or in .env (WZ__QUOTES_BASE_URL)")
|
55
|
+
|
56
|
+
# Construct the WebSocket URL.
|
57
|
+
self.url = f"{self.base_url}?token={self.token}"
|
58
|
+
self.ws: Optional[websockets.WebSocketClientProtocol] = None
|
59
|
+
self.on_tick: Optional[Callable[[dict], None]] = None
|
60
|
+
self.subscribed_instruments: set = set()
|
61
|
+
|
62
|
+
# Backoff configuration for reconnection (in seconds)
|
63
|
+
self._backoff_base = 1
|
64
|
+
self._backoff_factor = 2
|
65
|
+
self._backoff_max = 60
|
66
|
+
|
67
|
+
logger.debug("Initialized QuotesClient with URL: %s", self.url)
|
68
|
+
|
69
|
+
async def connect(self) -> None:
|
70
|
+
"""
|
71
|
+
Continuously connect to the quotes server and process incoming messages.
|
72
|
+
Implements an exponential backoff reconnection strategy.
|
73
|
+
"""
|
74
|
+
backoff = self._backoff_base
|
75
|
+
|
76
|
+
while True:
|
77
|
+
try:
|
78
|
+
logger.info("Connecting to %s ...", self.url)
|
79
|
+
async with websockets.connect(self.url) as websocket:
|
80
|
+
self.ws = websocket
|
81
|
+
logger.info("Connected to the quotes server.")
|
82
|
+
|
83
|
+
# On reconnection, re-subscribe if needed.
|
84
|
+
if self.subscribed_instruments:
|
85
|
+
subscribe_msg = {
|
86
|
+
"action": "subscribe",
|
87
|
+
"instruments": list(self.subscribed_instruments)
|
88
|
+
}
|
89
|
+
await self.ws.send(json.dumps(subscribe_msg))
|
90
|
+
logger.info("Re-subscribed to instruments: %s", list(self.subscribed_instruments))
|
91
|
+
|
92
|
+
# Reset backoff after a successful connection.
|
93
|
+
backoff = self._backoff_base
|
94
|
+
|
95
|
+
await self._handle_messages()
|
96
|
+
except ConnectionClosed as e:
|
97
|
+
logger.info("Disconnected from the quotes server: %s", e)
|
98
|
+
except Exception as e:
|
99
|
+
logger.error("Connection error: %s", e, exc_info=True)
|
100
|
+
|
101
|
+
# Exponential backoff before reconnecting.
|
102
|
+
sleep_time = min(backoff, self._backoff_max)
|
103
|
+
logger.info("Reconnecting in %s seconds...", sleep_time)
|
104
|
+
await asyncio.sleep(sleep_time)
|
105
|
+
backoff *= self._backoff_factor
|
106
|
+
# Add a bit of randomness to avoid thundering herd issues.
|
107
|
+
backoff += random.uniform(0, 1)
|
108
|
+
|
109
|
+
async def _handle_messages(self) -> None:
|
110
|
+
"""
|
111
|
+
Handle incoming messages and dispatch them via the on_tick callback.
|
112
|
+
"""
|
113
|
+
try:
|
114
|
+
async for message in self.ws: # type: ignore
|
115
|
+
try:
|
116
|
+
tick = json.loads(message)
|
117
|
+
if self.on_tick:
|
118
|
+
self.on_tick(tick)
|
119
|
+
else:
|
120
|
+
logger.debug("Received tick (no on_tick callback set): %s", tick)
|
121
|
+
except json.JSONDecodeError:
|
122
|
+
logger.debug("Received non-JSON message: %s", message)
|
123
|
+
except ConnectionClosed as e:
|
124
|
+
logger.info("Connection closed during message handling: %s", e)
|
125
|
+
|
126
|
+
async def subscribe(self, instruments: List[str]) -> None:
|
127
|
+
"""
|
128
|
+
Subscribe to a list of instruments and update the subscription list.
|
129
|
+
|
130
|
+
Args:
|
131
|
+
instruments (List[str]): List of instrument identifiers.
|
132
|
+
"""
|
133
|
+
if self.ws and self.ws.state == State.OPEN:
|
134
|
+
new_instruments = set(instruments) - self.subscribed_instruments
|
135
|
+
if new_instruments:
|
136
|
+
self.subscribed_instruments.update(new_instruments)
|
137
|
+
message = {"action": "subscribe", "instruments": list(new_instruments)}
|
138
|
+
await self.ws.send(json.dumps(message))
|
139
|
+
logger.info("Sent subscription message for instruments: %s", list(new_instruments))
|
140
|
+
else:
|
141
|
+
logger.info("Instruments already subscribed: %s", instruments)
|
142
|
+
else:
|
143
|
+
logger.info("Cannot subscribe: WebSocket is not connected.")
|
144
|
+
|
145
|
+
async def unsubscribe(self, instruments: List[str]) -> None:
|
146
|
+
"""
|
147
|
+
Unsubscribe from a list of instruments and update the subscription list.
|
148
|
+
|
149
|
+
Args:
|
150
|
+
instruments (List[str]): List of instrument identifiers.
|
151
|
+
"""
|
152
|
+
if self.ws and self.ws.state == State.OPEN:
|
153
|
+
unsub_set = set(instruments)
|
154
|
+
if unsub_set & self.subscribed_instruments:
|
155
|
+
self.subscribed_instruments.difference_update(unsub_set)
|
156
|
+
message = {"action": "unsubscribe", "instruments": list(unsub_set)}
|
157
|
+
await self.ws.send(json.dumps(message))
|
158
|
+
logger.info("Sent unsubscription message for instruments: %s", list(unsub_set))
|
159
|
+
else:
|
160
|
+
logger.info("No matching instruments found in current subscription.")
|
161
|
+
else:
|
162
|
+
logger.info("Cannot unsubscribe: WebSocket is not connected.")
|
163
|
+
|
164
|
+
async def close(self) -> None:
|
165
|
+
"""
|
166
|
+
Close the WebSocket connection.
|
167
|
+
"""
|
168
|
+
if self.ws:
|
169
|
+
await self.ws.close()
|
170
|
+
logger.info("WebSocket connection closed.")
|
@@ -0,0 +1,43 @@
|
|
1
|
+
Metadata-Version: 2.2
|
2
|
+
Name: wiz_trader
|
3
|
+
Version: 0.1.0
|
4
|
+
Summary: A Python SDK for connecting to the Wizzer.
|
5
|
+
Home-page: https://bitbucket.org/wizzer-tech/quotes_sdk.git
|
6
|
+
Author: Pawan Wagh
|
7
|
+
Author-email: pawan@wizzer.in
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
9
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
10
|
+
Classifier: Intended Audience :: Developers
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
12
|
+
Classifier: Operating System :: OS Independent
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
14
|
+
Classifier: Topic :: Office/Business :: Financial
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
16
|
+
Requires-Python: >=3.6
|
17
|
+
Description-Content-Type: text/markdown
|
18
|
+
Requires-Dist: websockets
|
19
|
+
Requires-Dist: python-dotenv
|
20
|
+
Dynamic: author
|
21
|
+
Dynamic: author-email
|
22
|
+
Dynamic: classifier
|
23
|
+
Dynamic: description
|
24
|
+
Dynamic: description-content-type
|
25
|
+
Dynamic: home-page
|
26
|
+
Dynamic: requires-dist
|
27
|
+
Dynamic: requires-python
|
28
|
+
Dynamic: summary
|
29
|
+
|
30
|
+
# WizTrader SDK
|
31
|
+
|
32
|
+
A Python SDK for connecting to the Wizzer.
|
33
|
+
|
34
|
+
## Installation
|
35
|
+
|
36
|
+
Install the dependencies:
|
37
|
+
```
|
38
|
+
pip install -r requirements.txt
|
39
|
+
```
|
40
|
+
|
41
|
+
## Usage Example
|
42
|
+
|
43
|
+
See `example.py` for a complete example.
|
@@ -0,0 +1,8 @@
|
|
1
|
+
apis/__init__.py,sha256=30bmGcN4MBabbN1-7wuowVRfwjuhdszhnF6uOoyIJoU,109
|
2
|
+
apis/client.py,sha256=NJ9cPIK0LIe1rhC8CIRIXRvQ3zksIuNJCFlnTJlYm9E,88
|
3
|
+
ticker/__init__.py,sha256=f4xaHOWLxEyckZpHbZFWHUbiaqqOTJ318Xi8vDJWGdg,99
|
4
|
+
ticker/client.py,sha256=DR0RIqmLjuo3ZDs2rVzMib-NRyjh96Xq09ZhI7vR56k,6245
|
5
|
+
wiz_trader-0.1.0.dist-info/METADATA,sha256=d36YvgtqisRnNhdZ9HyECOYGHwJ-TNqlKznMD_5AT1k,1158
|
6
|
+
wiz_trader-0.1.0.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
7
|
+
wiz_trader-0.1.0.dist-info/top_level.txt,sha256=VDgCJWC-MmXn4pnnJXeasXwqKHN0Qhu0TmbEPbGU2Ro,12
|
8
|
+
wiz_trader-0.1.0.dist-info/RECORD,,
|