hotstuff-python-sdk 0.0.1b1__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.
- hotstuff/__init__.py +53 -0
- hotstuff/apis/__init__.py +10 -0
- hotstuff/apis/exchange.py +510 -0
- hotstuff/apis/info.py +291 -0
- hotstuff/apis/subscription.py +278 -0
- hotstuff/methods/__init__.py +10 -0
- hotstuff/methods/exchange/__init__.py +14 -0
- hotstuff/methods/exchange/account.py +120 -0
- hotstuff/methods/exchange/collateral.py +94 -0
- hotstuff/methods/exchange/op_codes.py +28 -0
- hotstuff/methods/exchange/trading.py +117 -0
- hotstuff/methods/exchange/vault.py +59 -0
- hotstuff/methods/info/__init__.py +14 -0
- hotstuff/methods/info/account.py +371 -0
- hotstuff/methods/info/explorer.py +57 -0
- hotstuff/methods/info/global.py +249 -0
- hotstuff/methods/info/vault.py +86 -0
- hotstuff/methods/subscription/__init__.py +8 -0
- hotstuff/methods/subscription/global.py +200 -0
- hotstuff/transports/__init__.py +9 -0
- hotstuff/transports/http.py +142 -0
- hotstuff/transports/websocket.py +401 -0
- hotstuff/types/__init__.py +62 -0
- hotstuff/types/clients.py +34 -0
- hotstuff/types/exchange.py +88 -0
- hotstuff/types/transports.py +110 -0
- hotstuff/utils/__init__.py +13 -0
- hotstuff/utils/address.py +37 -0
- hotstuff/utils/endpoints.py +15 -0
- hotstuff/utils/nonce.py +25 -0
- hotstuff/utils/signing.py +76 -0
- hotstuff_python_sdk-0.0.1b1.dist-info/LICENSE +21 -0
- hotstuff_python_sdk-0.0.1b1.dist-info/METADATA +985 -0
- hotstuff_python_sdk-0.0.1b1.dist-info/RECORD +35 -0
- hotstuff_python_sdk-0.0.1b1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"""WebSocket transport implementation."""
|
|
2
|
+
import asyncio
|
|
3
|
+
import json
|
|
4
|
+
from typing import Optional, Dict, Any, Callable, List
|
|
5
|
+
import websockets
|
|
6
|
+
from websockets.client import WebSocketClientProtocol
|
|
7
|
+
|
|
8
|
+
from hotstuff.types import (
|
|
9
|
+
WebSocketTransportOptions,
|
|
10
|
+
JSONRPCMessage,
|
|
11
|
+
JSONRPCResponse,
|
|
12
|
+
JSONRPCNotification,
|
|
13
|
+
Subscription,
|
|
14
|
+
SubscriptionData,
|
|
15
|
+
WSMethod,
|
|
16
|
+
SubscribeResult,
|
|
17
|
+
UnsubscribeResult,
|
|
18
|
+
PongResult,
|
|
19
|
+
)
|
|
20
|
+
from hotstuff.utils import ENDPOINTS_URLS
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class WebSocketTransport:
|
|
24
|
+
"""WebSocket transport for real-time subscriptions."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, options: Optional[WebSocketTransportOptions] = None):
|
|
27
|
+
"""
|
|
28
|
+
Initialize WebSocket transport.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
options: Transport configuration options
|
|
32
|
+
"""
|
|
33
|
+
options = options or WebSocketTransportOptions()
|
|
34
|
+
|
|
35
|
+
self.is_testnet = options.is_testnet
|
|
36
|
+
self.timeout = options.timeout
|
|
37
|
+
|
|
38
|
+
# Setup server endpoints
|
|
39
|
+
self.server = {
|
|
40
|
+
"mainnet": ENDPOINTS_URLS["mainnet"]["ws"],
|
|
41
|
+
"testnet": ENDPOINTS_URLS["testnet"]["ws"],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if options.server:
|
|
45
|
+
if "mainnet" in options.server:
|
|
46
|
+
self.server["mainnet"] = options.server["mainnet"]
|
|
47
|
+
if "testnet" in options.server:
|
|
48
|
+
self.server["testnet"] = options.server["testnet"]
|
|
49
|
+
|
|
50
|
+
self.keep_alive = options.keep_alive or {
|
|
51
|
+
"interval": 30.0,
|
|
52
|
+
"timeout": 10.0,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
self.ws: Optional[WebSocketClientProtocol] = None
|
|
56
|
+
self.reconnect_attempts = 0
|
|
57
|
+
self.max_reconnect_attempts = 5
|
|
58
|
+
self.reconnect_delay = 1.0
|
|
59
|
+
|
|
60
|
+
self.message_queue: Dict[str, asyncio.Future] = {}
|
|
61
|
+
self.message_id_counter = 0
|
|
62
|
+
|
|
63
|
+
self.subscriptions: Dict[str, Subscription] = {}
|
|
64
|
+
self.subscription_callbacks: Dict[str, Callable] = {}
|
|
65
|
+
|
|
66
|
+
self.connection_promise: Optional[asyncio.Task] = None
|
|
67
|
+
self.keep_alive_task: Optional[asyncio.Task] = None
|
|
68
|
+
self.receive_task: Optional[asyncio.Task] = None
|
|
69
|
+
|
|
70
|
+
self.auto_connect = options.auto_connect
|
|
71
|
+
if self.auto_connect:
|
|
72
|
+
# Don't await here, just schedule it
|
|
73
|
+
asyncio.create_task(self._auto_connect())
|
|
74
|
+
|
|
75
|
+
async def _auto_connect(self):
|
|
76
|
+
"""Auto-connect on initialization."""
|
|
77
|
+
try:
|
|
78
|
+
await self.connect()
|
|
79
|
+
except Exception as e:
|
|
80
|
+
print(f"Auto-connect failed: {e}")
|
|
81
|
+
|
|
82
|
+
async def _ensure_connected(self):
|
|
83
|
+
"""Ensure the WebSocket is connected."""
|
|
84
|
+
if self.ws and not self.ws.closed:
|
|
85
|
+
return
|
|
86
|
+
|
|
87
|
+
if self.connection_promise:
|
|
88
|
+
await self.connection_promise
|
|
89
|
+
return
|
|
90
|
+
|
|
91
|
+
await self.connect()
|
|
92
|
+
|
|
93
|
+
def _cleanup(self):
|
|
94
|
+
"""Cleanup resources."""
|
|
95
|
+
if self.keep_alive_task:
|
|
96
|
+
self.keep_alive_task.cancel()
|
|
97
|
+
self.keep_alive_task = None
|
|
98
|
+
|
|
99
|
+
if self.receive_task:
|
|
100
|
+
self.receive_task.cancel()
|
|
101
|
+
self.receive_task = None
|
|
102
|
+
|
|
103
|
+
# Reject all pending messages
|
|
104
|
+
for future in self.message_queue.values():
|
|
105
|
+
if not future.done():
|
|
106
|
+
future.set_exception(Exception("WebSocket disconnected"))
|
|
107
|
+
|
|
108
|
+
self.message_queue.clear()
|
|
109
|
+
|
|
110
|
+
async def _start_keep_alive(self):
|
|
111
|
+
"""Start keep-alive ping loop."""
|
|
112
|
+
interval = self.keep_alive.get("interval")
|
|
113
|
+
if not interval:
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
while True:
|
|
117
|
+
try:
|
|
118
|
+
await asyncio.sleep(interval)
|
|
119
|
+
await self.ping()
|
|
120
|
+
except asyncio.CancelledError:
|
|
121
|
+
break
|
|
122
|
+
except Exception as e:
|
|
123
|
+
print(f"Keep-alive error: {e}")
|
|
124
|
+
break
|
|
125
|
+
|
|
126
|
+
def _handle_incoming_message(self, message: dict):
|
|
127
|
+
"""Handle incoming WebSocket message."""
|
|
128
|
+
# Check if it's a JSON-RPC response
|
|
129
|
+
if "id" in message and ("result" in message or "error" in message):
|
|
130
|
+
self._handle_jsonrpc_response(message)
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
# Check if it's a notification
|
|
134
|
+
if "method" in message and "params" in message and "id" not in message:
|
|
135
|
+
self._handle_jsonrpc_notification(message)
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
def _handle_jsonrpc_response(self, response: dict):
|
|
139
|
+
"""Handle JSON-RPC response."""
|
|
140
|
+
msg_id = str(response.get("id"))
|
|
141
|
+
future = self.message_queue.pop(msg_id, None)
|
|
142
|
+
|
|
143
|
+
if future and not future.done():
|
|
144
|
+
if "error" in response:
|
|
145
|
+
error = response["error"]
|
|
146
|
+
future.set_exception(
|
|
147
|
+
Exception(f"JSON-RPC Error {error.get('code')}: {error.get('message')}")
|
|
148
|
+
)
|
|
149
|
+
else:
|
|
150
|
+
future.set_result(response.get("result"))
|
|
151
|
+
|
|
152
|
+
def _handle_jsonrpc_notification(self, notification: dict):
|
|
153
|
+
"""Handle JSON-RPC notification."""
|
|
154
|
+
method = notification.get("method")
|
|
155
|
+
params = notification.get("params")
|
|
156
|
+
|
|
157
|
+
if method in ("subscription", "event") and params:
|
|
158
|
+
channel = params.get("channel")
|
|
159
|
+
data = params.get("data")
|
|
160
|
+
|
|
161
|
+
# Find matching subscriptions
|
|
162
|
+
for sub_id, subscription in self.subscriptions.items():
|
|
163
|
+
if subscription.channel == channel:
|
|
164
|
+
callback = self.subscription_callbacks.get(sub_id)
|
|
165
|
+
if callback:
|
|
166
|
+
subscription_data = SubscriptionData(
|
|
167
|
+
channel=channel,
|
|
168
|
+
data=data,
|
|
169
|
+
timestamp=asyncio.get_event_loop().time()
|
|
170
|
+
)
|
|
171
|
+
try:
|
|
172
|
+
callback(subscription_data)
|
|
173
|
+
except Exception as e:
|
|
174
|
+
print(f"Callback error: {e}")
|
|
175
|
+
|
|
176
|
+
async def _receive_messages(self):
|
|
177
|
+
"""Receive messages from WebSocket."""
|
|
178
|
+
try:
|
|
179
|
+
async for message in self.ws:
|
|
180
|
+
try:
|
|
181
|
+
data = json.loads(message)
|
|
182
|
+
self._handle_incoming_message(data)
|
|
183
|
+
except json.JSONDecodeError as e:
|
|
184
|
+
print(f"Failed to parse message: {e}")
|
|
185
|
+
except Exception as e:
|
|
186
|
+
print(f"Error handling message: {e}")
|
|
187
|
+
except asyncio.CancelledError:
|
|
188
|
+
pass
|
|
189
|
+
except Exception as e:
|
|
190
|
+
print(f"Receive error: {e}")
|
|
191
|
+
# Trigger reconnection
|
|
192
|
+
if self.reconnect_attempts < self.max_reconnect_attempts:
|
|
193
|
+
asyncio.create_task(self._reconnect())
|
|
194
|
+
|
|
195
|
+
async def _reconnect(self):
|
|
196
|
+
"""Reconnect to WebSocket."""
|
|
197
|
+
self._cleanup()
|
|
198
|
+
await asyncio.sleep(self.reconnect_delay * self.reconnect_attempts)
|
|
199
|
+
self.reconnect_attempts += 1
|
|
200
|
+
await self.connect()
|
|
201
|
+
|
|
202
|
+
async def _send_jsonrpc_message(self, message: dict) -> Any:
|
|
203
|
+
"""Send a JSON-RPC message and wait for response."""
|
|
204
|
+
await self._ensure_connected()
|
|
205
|
+
|
|
206
|
+
# Assign message ID if not present
|
|
207
|
+
if "id" not in message or message["id"] is None:
|
|
208
|
+
self.message_id_counter += 1
|
|
209
|
+
message["id"] = str(self.message_id_counter)
|
|
210
|
+
|
|
211
|
+
msg_id = str(message["id"])
|
|
212
|
+
|
|
213
|
+
# Create future for response
|
|
214
|
+
future = asyncio.Future()
|
|
215
|
+
self.message_queue[msg_id] = future
|
|
216
|
+
|
|
217
|
+
# Set timeout
|
|
218
|
+
if self.timeout:
|
|
219
|
+
async def timeout_handler():
|
|
220
|
+
await asyncio.sleep(self.timeout)
|
|
221
|
+
if msg_id in self.message_queue:
|
|
222
|
+
self.message_queue.pop(msg_id)
|
|
223
|
+
if not future.done():
|
|
224
|
+
future.set_exception(Exception("Request timeout"))
|
|
225
|
+
|
|
226
|
+
asyncio.create_task(timeout_handler())
|
|
227
|
+
|
|
228
|
+
# Send message
|
|
229
|
+
await self.ws.send(json.dumps(message))
|
|
230
|
+
|
|
231
|
+
# Wait for response
|
|
232
|
+
return await future
|
|
233
|
+
|
|
234
|
+
def _format_subscription_params(
|
|
235
|
+
self,
|
|
236
|
+
channel: str,
|
|
237
|
+
payload: dict
|
|
238
|
+
) -> dict:
|
|
239
|
+
"""Format subscription parameters."""
|
|
240
|
+
subscription = {
|
|
241
|
+
"channel": channel,
|
|
242
|
+
**payload,
|
|
243
|
+
}
|
|
244
|
+
return subscription
|
|
245
|
+
|
|
246
|
+
async def _subscribe_to_channels(self, params: dict) -> SubscribeResult:
|
|
247
|
+
"""Subscribe to channels."""
|
|
248
|
+
self.message_id_counter += 1
|
|
249
|
+
message = {
|
|
250
|
+
"jsonrpc": "2.0",
|
|
251
|
+
"method": WSMethod.SUBSCRIBE,
|
|
252
|
+
"params": params,
|
|
253
|
+
"id": str(self.message_id_counter),
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
result = await self._send_jsonrpc_message(message)
|
|
257
|
+
return SubscribeResult(**result) if isinstance(result, dict) else result
|
|
258
|
+
|
|
259
|
+
async def _unsubscribe_from_channels(self, channels: List[str]) -> UnsubscribeResult:
|
|
260
|
+
"""Unsubscribe from channels."""
|
|
261
|
+
self.message_id_counter += 1
|
|
262
|
+
message = {
|
|
263
|
+
"jsonrpc": "2.0",
|
|
264
|
+
"method": WSMethod.UNSUBSCRIBE,
|
|
265
|
+
"params": channels,
|
|
266
|
+
"id": str(self.message_id_counter),
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
result = await self._send_jsonrpc_message(message)
|
|
270
|
+
return UnsubscribeResult(**result) if isinstance(result, dict) else result
|
|
271
|
+
|
|
272
|
+
def is_connected(self) -> bool:
|
|
273
|
+
"""Check if WebSocket is connected."""
|
|
274
|
+
return self.ws is not None and not self.ws.closed
|
|
275
|
+
|
|
276
|
+
async def connect(self):
|
|
277
|
+
"""Connect to WebSocket server."""
|
|
278
|
+
url = self.server["testnet" if self.is_testnet else "mainnet"]
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
self.ws = await websockets.connect(url)
|
|
282
|
+
self.reconnect_attempts = 0
|
|
283
|
+
|
|
284
|
+
# Start keep-alive
|
|
285
|
+
if self.keep_alive.get("interval"):
|
|
286
|
+
self.keep_alive_task = asyncio.create_task(self._start_keep_alive())
|
|
287
|
+
|
|
288
|
+
# Start receiving messages
|
|
289
|
+
self.receive_task = asyncio.create_task(self._receive_messages())
|
|
290
|
+
|
|
291
|
+
except Exception as e:
|
|
292
|
+
raise Exception(f"Failed to connect: {e}")
|
|
293
|
+
|
|
294
|
+
async def disconnect(self):
|
|
295
|
+
"""Disconnect from WebSocket server."""
|
|
296
|
+
self._cleanup()
|
|
297
|
+
|
|
298
|
+
if self.ws:
|
|
299
|
+
await self.ws.close()
|
|
300
|
+
self.ws = None
|
|
301
|
+
|
|
302
|
+
async def ping(self) -> PongResult:
|
|
303
|
+
"""Send ping to server."""
|
|
304
|
+
self.message_id_counter += 1
|
|
305
|
+
message = {
|
|
306
|
+
"jsonrpc": "2.0",
|
|
307
|
+
"method": WSMethod.PING,
|
|
308
|
+
"id": str(self.message_id_counter),
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
result = await self._send_jsonrpc_message(message)
|
|
312
|
+
return PongResult(pong=True)
|
|
313
|
+
|
|
314
|
+
async def subscribe(
|
|
315
|
+
self,
|
|
316
|
+
channel: str,
|
|
317
|
+
payload: dict,
|
|
318
|
+
listener: Callable
|
|
319
|
+
) -> Dict[str, Any]:
|
|
320
|
+
"""
|
|
321
|
+
Subscribe to a channel.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
channel: The channel to subscribe to
|
|
325
|
+
payload: Subscription parameters
|
|
326
|
+
listener: Callback function for updates
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
Subscription result with unsubscribe method
|
|
330
|
+
"""
|
|
331
|
+
await self._ensure_connected()
|
|
332
|
+
|
|
333
|
+
subscription_id = f"{channel}_{asyncio.get_event_loop().time()}"
|
|
334
|
+
|
|
335
|
+
subscription = Subscription(
|
|
336
|
+
id=subscription_id,
|
|
337
|
+
channel=channel,
|
|
338
|
+
symbol=payload.get("instrumentId") or payload.get("symbol"),
|
|
339
|
+
params=payload,
|
|
340
|
+
timestamp=asyncio.get_event_loop().time()
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
self.subscription_callbacks[subscription_id] = listener
|
|
344
|
+
|
|
345
|
+
try:
|
|
346
|
+
subscription_params = self._format_subscription_params(channel, payload)
|
|
347
|
+
result = await self._subscribe_to_channels(subscription_params)
|
|
348
|
+
|
|
349
|
+
if result.status == "subscribed" and result.channels:
|
|
350
|
+
server_channel = result.channels[0]
|
|
351
|
+
subscription.channel = server_channel
|
|
352
|
+
self.subscriptions[subscription_id] = subscription
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
"subscriptionId": subscription_id,
|
|
356
|
+
"status": result.status,
|
|
357
|
+
"channels": result.channels,
|
|
358
|
+
"unsubscribe": lambda: self.unsubscribe(subscription_id),
|
|
359
|
+
}
|
|
360
|
+
else:
|
|
361
|
+
self.subscription_callbacks.pop(subscription_id, None)
|
|
362
|
+
error_msg = result.error or f"Subscription {result.status}"
|
|
363
|
+
raise Exception(f"Server rejected subscription: {error_msg}")
|
|
364
|
+
|
|
365
|
+
except Exception as e:
|
|
366
|
+
self.subscriptions.pop(subscription_id, None)
|
|
367
|
+
self.subscription_callbacks.pop(subscription_id, None)
|
|
368
|
+
raise e
|
|
369
|
+
|
|
370
|
+
async def unsubscribe(self, subscription_id: str):
|
|
371
|
+
"""Unsubscribe from a channel."""
|
|
372
|
+
subscription = self.subscriptions.get(subscription_id)
|
|
373
|
+
if not subscription:
|
|
374
|
+
raise Exception(f"Subscription {subscription_id} not found")
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
if self.is_connected():
|
|
378
|
+
await self._unsubscribe_from_channels([subscription.channel])
|
|
379
|
+
|
|
380
|
+
self.subscriptions.pop(subscription_id, None)
|
|
381
|
+
self.subscription_callbacks.pop(subscription_id, None)
|
|
382
|
+
|
|
383
|
+
except Exception as e:
|
|
384
|
+
print(f"Failed to unsubscribe: {e}")
|
|
385
|
+
self.subscriptions.pop(subscription_id, None)
|
|
386
|
+
self.subscription_callbacks.pop(subscription_id, None)
|
|
387
|
+
raise e
|
|
388
|
+
|
|
389
|
+
def get_subscriptions(self) -> List[Subscription]:
|
|
390
|
+
"""Get all active subscriptions."""
|
|
391
|
+
return list(self.subscriptions.values())
|
|
392
|
+
|
|
393
|
+
async def __aenter__(self):
|
|
394
|
+
"""Async context manager entry."""
|
|
395
|
+
await self.connect()
|
|
396
|
+
return self
|
|
397
|
+
|
|
398
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
399
|
+
"""Async context manager exit."""
|
|
400
|
+
await self.disconnect()
|
|
401
|
+
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Type definitions package."""
|
|
2
|
+
from hotstuff.types.transports import (
|
|
3
|
+
HttpTransportOptions,
|
|
4
|
+
WebSocketTransportOptions,
|
|
5
|
+
JSONRPCMessage,
|
|
6
|
+
JSONRPCResponse,
|
|
7
|
+
JSONRPCNotification,
|
|
8
|
+
SubscriptionData,
|
|
9
|
+
Subscription,
|
|
10
|
+
WSMethod,
|
|
11
|
+
SubscribeResult,
|
|
12
|
+
UnsubscribeResult,
|
|
13
|
+
PongResult,
|
|
14
|
+
)
|
|
15
|
+
from hotstuff.types.clients import (
|
|
16
|
+
InfoClientParameters,
|
|
17
|
+
ExchangeClientParameters,
|
|
18
|
+
SubscriptionClientParameters,
|
|
19
|
+
ActionRequest,
|
|
20
|
+
)
|
|
21
|
+
from hotstuff.types.exchange import (
|
|
22
|
+
UnitOrder,
|
|
23
|
+
BrokerConfig,
|
|
24
|
+
PlaceOrderParams,
|
|
25
|
+
UnitCancelByOrderId,
|
|
26
|
+
CancelByOidParams,
|
|
27
|
+
UnitCancelByClOrderId,
|
|
28
|
+
CancelByCloidParams,
|
|
29
|
+
CancelAllParams,
|
|
30
|
+
AddAgentParams,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
# Transport types
|
|
35
|
+
"HttpTransportOptions",
|
|
36
|
+
"WebSocketTransportOptions",
|
|
37
|
+
"JSONRPCMessage",
|
|
38
|
+
"JSONRPCResponse",
|
|
39
|
+
"JSONRPCNotification",
|
|
40
|
+
"SubscriptionData",
|
|
41
|
+
"Subscription",
|
|
42
|
+
"WSMethod",
|
|
43
|
+
"SubscribeResult",
|
|
44
|
+
"UnsubscribeResult",
|
|
45
|
+
"PongResult",
|
|
46
|
+
# Client types
|
|
47
|
+
"InfoClientParameters",
|
|
48
|
+
"ExchangeClientParameters",
|
|
49
|
+
"SubscriptionClientParameters",
|
|
50
|
+
"ActionRequest",
|
|
51
|
+
# Exchange types
|
|
52
|
+
"UnitOrder",
|
|
53
|
+
"BrokerConfig",
|
|
54
|
+
"PlaceOrderParams",
|
|
55
|
+
"UnitCancelByOrderId",
|
|
56
|
+
"CancelByOidParams",
|
|
57
|
+
"UnitCancelByClOrderId",
|
|
58
|
+
"CancelByCloidParams",
|
|
59
|
+
"CancelAllParams",
|
|
60
|
+
"AddAgentParams",
|
|
61
|
+
]
|
|
62
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Type definitions for client parameters."""
|
|
2
|
+
from typing import TypeVar, Generic, Optional, Callable, Awaitable, Any
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
# Type variable for transport
|
|
6
|
+
T = TypeVar('T')
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class InfoClientParameters(Generic[T]):
|
|
11
|
+
"""Parameters for InfoClient."""
|
|
12
|
+
transport: T
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ExchangeClientParameters(Generic[T]):
|
|
17
|
+
"""Parameters for ExchangeClient."""
|
|
18
|
+
transport: T
|
|
19
|
+
wallet: Any
|
|
20
|
+
nonce: Optional[Callable[[], Awaitable[int]]] = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class SubscriptionClientParameters(Generic[T]):
|
|
25
|
+
"""Parameters for SubscriptionClient."""
|
|
26
|
+
transport: T
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class ActionRequest:
|
|
31
|
+
"""Action request."""
|
|
32
|
+
action: str
|
|
33
|
+
params: dict
|
|
34
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Type definitions for exchange methods."""
|
|
2
|
+
from typing import Optional, List
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class UnitOrder:
|
|
8
|
+
"""Single order unit."""
|
|
9
|
+
instrument_id: int
|
|
10
|
+
side: str # 'b' or 's'
|
|
11
|
+
position_side: str # 'LONG', 'SHORT', or 'BOTH'
|
|
12
|
+
price: str
|
|
13
|
+
size: str
|
|
14
|
+
tif: str # 'GTC', 'IOC', or 'FOK'
|
|
15
|
+
ro: bool # reduce-only
|
|
16
|
+
po: bool # post-only
|
|
17
|
+
cloid: str # client order ID
|
|
18
|
+
trigger_px: Optional[str] = None
|
|
19
|
+
is_market: Optional[bool] = None
|
|
20
|
+
tpsl: Optional[str] = None # 'tp', 'sl', or ''
|
|
21
|
+
grouping: Optional[str] = None # 'position', 'normal', or ''
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class BrokerConfig:
|
|
26
|
+
"""Broker configuration."""
|
|
27
|
+
broker: str
|
|
28
|
+
fee: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class PlaceOrderParams:
|
|
33
|
+
"""Parameters for placing an order."""
|
|
34
|
+
orders: List[UnitOrder]
|
|
35
|
+
expiresAfter: int
|
|
36
|
+
broker_config: Optional[BrokerConfig] = None
|
|
37
|
+
nonce: Optional[int] = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class UnitCancelByOrderId:
|
|
42
|
+
"""Cancel by order ID unit."""
|
|
43
|
+
oid: int
|
|
44
|
+
instrument_id: int
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class CancelByOidParams:
|
|
49
|
+
"""Parameters for cancelling by order ID."""
|
|
50
|
+
cancels: List[UnitCancelByOrderId]
|
|
51
|
+
expiresAfter: int
|
|
52
|
+
nonce: Optional[int] = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class UnitCancelByClOrderId:
|
|
57
|
+
"""Cancel by client order ID unit."""
|
|
58
|
+
cloid: str
|
|
59
|
+
instrument_id: int
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@dataclass
|
|
63
|
+
class CancelByCloidParams:
|
|
64
|
+
"""Parameters for cancelling by client order ID."""
|
|
65
|
+
cancels: List[UnitCancelByClOrderId]
|
|
66
|
+
expiresAfter: int
|
|
67
|
+
nonce: Optional[int] = None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass
|
|
71
|
+
class CancelAllParams:
|
|
72
|
+
"""Parameters for cancelling all orders."""
|
|
73
|
+
expiresAfter: int
|
|
74
|
+
nonce: Optional[int] = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass
|
|
78
|
+
class AddAgentParams:
|
|
79
|
+
"""Parameters for adding an agent."""
|
|
80
|
+
agent_name: str
|
|
81
|
+
agent: str
|
|
82
|
+
for_account: str
|
|
83
|
+
agent_private_key: str
|
|
84
|
+
signer: str
|
|
85
|
+
valid_until: int
|
|
86
|
+
signature: Optional[str] = None
|
|
87
|
+
nonce: Optional[int] = None
|
|
88
|
+
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Type definitions for transports."""
|
|
2
|
+
from typing import Optional, Dict, Any, Callable, Awaitable, Union
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class HttpTransportOptions:
|
|
8
|
+
"""Options for HTTP transport configuration."""
|
|
9
|
+
is_testnet: bool = False
|
|
10
|
+
timeout: Optional[float] = 3.0
|
|
11
|
+
server: Optional[Dict[str, Dict[str, str]]] = None
|
|
12
|
+
headers: Optional[Dict[str, str]] = None
|
|
13
|
+
on_request: Optional[Callable] = None
|
|
14
|
+
on_response: Optional[Callable] = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class WebSocketTransportOptions:
|
|
19
|
+
"""Options for WebSocket transport configuration."""
|
|
20
|
+
is_testnet: bool = False
|
|
21
|
+
timeout: Optional[float] = 10.0
|
|
22
|
+
server: Optional[Dict[str, str]] = None
|
|
23
|
+
keep_alive: Optional[Dict[str, Optional[float]]] = None
|
|
24
|
+
auto_connect: bool = True
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class JSONRPCMessage:
|
|
29
|
+
"""JSON-RPC 2.0 message."""
|
|
30
|
+
jsonrpc: str = "2.0"
|
|
31
|
+
method: Optional[str] = None
|
|
32
|
+
params: Optional[Union[Dict[str, Any], list]] = None
|
|
33
|
+
id: Optional[Union[str, int]] = None
|
|
34
|
+
result: Optional[Any] = None
|
|
35
|
+
error: Optional[Dict[str, Any]] = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class JSONRPCResponse:
|
|
40
|
+
"""JSON-RPC 2.0 response."""
|
|
41
|
+
jsonrpc: str
|
|
42
|
+
id: Union[str, int]
|
|
43
|
+
result: Optional[Any] = None
|
|
44
|
+
error: Optional[Dict[str, Any]] = None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class JSONRPCNotification:
|
|
49
|
+
"""JSON-RPC 2.0 notification."""
|
|
50
|
+
jsonrpc: str
|
|
51
|
+
method: str
|
|
52
|
+
params: Optional[Dict[str, Any]] = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class SubscriptionData:
|
|
57
|
+
"""Subscription data."""
|
|
58
|
+
channel: str
|
|
59
|
+
data: Any
|
|
60
|
+
timestamp: float
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class Subscription:
|
|
65
|
+
"""Subscription information."""
|
|
66
|
+
id: str
|
|
67
|
+
channel: str
|
|
68
|
+
symbol: Optional[str]
|
|
69
|
+
params: Dict[str, Any]
|
|
70
|
+
timestamp: float
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class WSMethod:
|
|
74
|
+
"""WebSocket method names."""
|
|
75
|
+
SUBSCRIBE = "subscribe"
|
|
76
|
+
UNSUBSCRIBE = "unsubscribe"
|
|
77
|
+
PING = "ping"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class SubscribeResult:
|
|
82
|
+
"""Result of subscription."""
|
|
83
|
+
status: str
|
|
84
|
+
channels: Optional[list] = None
|
|
85
|
+
error: Optional[str] = None
|
|
86
|
+
|
|
87
|
+
def __init__(self, status: str, channels: Optional[list] = None, error: Optional[str] = None, **kwargs):
|
|
88
|
+
"""Initialize SubscribeResult, ignoring extra fields like 'id'."""
|
|
89
|
+
self.status = status
|
|
90
|
+
self.channels = channels
|
|
91
|
+
self.error = error
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class UnsubscribeResult:
|
|
96
|
+
"""Result of unsubscription."""
|
|
97
|
+
status: str
|
|
98
|
+
channels: Optional[list] = None
|
|
99
|
+
|
|
100
|
+
def __init__(self, status: str, channels: Optional[list] = None, **kwargs):
|
|
101
|
+
"""Initialize UnsubscribeResult, ignoring extra fields like 'id'."""
|
|
102
|
+
self.status = status
|
|
103
|
+
self.channels = channels
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass
|
|
107
|
+
class PongResult:
|
|
108
|
+
"""Pong response."""
|
|
109
|
+
pong: bool = True
|
|
110
|
+
|