walrasquant-lib 0.4.20__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.
- walrasquant/__init__.py +7 -0
- walrasquant/aggregation.py +449 -0
- walrasquant/backends/__init__.py +5 -0
- walrasquant/backends/db.py +109 -0
- walrasquant/backends/db_memory.py +61 -0
- walrasquant/backends/db_postgresql.py +321 -0
- walrasquant/backends/db_sqlite.py +310 -0
- walrasquant/base/__init__.py +24 -0
- walrasquant/base/api_client.py +46 -0
- walrasquant/base/connector.py +863 -0
- walrasquant/base/ems.py +794 -0
- walrasquant/base/exchange.py +213 -0
- walrasquant/base/oms.py +428 -0
- walrasquant/base/retry.py +220 -0
- walrasquant/base/sms.py +545 -0
- walrasquant/base/ws_client.py +408 -0
- walrasquant/config.py +284 -0
- walrasquant/constants.py +413 -0
- walrasquant/core/__init__.py +0 -0
- walrasquant/core/cache.py +688 -0
- walrasquant/core/clock.py +59 -0
- walrasquant/core/connection.py +41 -0
- walrasquant/core/entity.py +504 -0
- walrasquant/core/nautilius_core.py +103 -0
- walrasquant/core/registry.py +41 -0
- walrasquant/engine.py +745 -0
- walrasquant/error.py +34 -0
- walrasquant/exchange/__init__.py +13 -0
- walrasquant/exchange/base_factory.py +172 -0
- walrasquant/exchange/binance/__init__.py +30 -0
- walrasquant/exchange/binance/connector.py +1093 -0
- walrasquant/exchange/binance/constants.py +934 -0
- walrasquant/exchange/binance/ems.py +140 -0
- walrasquant/exchange/binance/error.py +48 -0
- walrasquant/exchange/binance/exchange.py +144 -0
- walrasquant/exchange/binance/factory.py +115 -0
- walrasquant/exchange/binance/oms.py +1807 -0
- walrasquant/exchange/binance/rest_api.py +1653 -0
- walrasquant/exchange/binance/schema.py +1063 -0
- walrasquant/exchange/binance/websockets.py +389 -0
- walrasquant/exchange/bitget/__init__.py +28 -0
- walrasquant/exchange/bitget/connector.py +578 -0
- walrasquant/exchange/bitget/constants.py +392 -0
- walrasquant/exchange/bitget/ems.py +202 -0
- walrasquant/exchange/bitget/error.py +36 -0
- walrasquant/exchange/bitget/exchange.py +128 -0
- walrasquant/exchange/bitget/factory.py +135 -0
- walrasquant/exchange/bitget/oms.py +1619 -0
- walrasquant/exchange/bitget/rest_api.py +610 -0
- walrasquant/exchange/bitget/schema.py +885 -0
- walrasquant/exchange/bitget/websockets.py +753 -0
- walrasquant/exchange/bybit/__init__.py +32 -0
- walrasquant/exchange/bybit/connector.py +819 -0
- walrasquant/exchange/bybit/constants.py +479 -0
- walrasquant/exchange/bybit/ems.py +93 -0
- walrasquant/exchange/bybit/error.py +36 -0
- walrasquant/exchange/bybit/exchange.py +108 -0
- walrasquant/exchange/bybit/factory.py +128 -0
- walrasquant/exchange/bybit/oms.py +1195 -0
- walrasquant/exchange/bybit/rest_api.py +570 -0
- walrasquant/exchange/bybit/schema.py +867 -0
- walrasquant/exchange/bybit/websockets.py +307 -0
- walrasquant/exchange/hyperliquid/__init__.py +28 -0
- walrasquant/exchange/hyperliquid/connector.py +370 -0
- walrasquant/exchange/hyperliquid/constants.py +371 -0
- walrasquant/exchange/hyperliquid/ems.py +156 -0
- walrasquant/exchange/hyperliquid/error.py +48 -0
- walrasquant/exchange/hyperliquid/exchange.py +120 -0
- walrasquant/exchange/hyperliquid/factory.py +135 -0
- walrasquant/exchange/hyperliquid/oms.py +1081 -0
- walrasquant/exchange/hyperliquid/rest_api.py +348 -0
- walrasquant/exchange/hyperliquid/schema.py +583 -0
- walrasquant/exchange/hyperliquid/websockets.py +592 -0
- walrasquant/exchange/okx/__init__.py +25 -0
- walrasquant/exchange/okx/connector.py +931 -0
- walrasquant/exchange/okx/constants.py +518 -0
- walrasquant/exchange/okx/ems.py +144 -0
- walrasquant/exchange/okx/error.py +66 -0
- walrasquant/exchange/okx/exchange.py +102 -0
- walrasquant/exchange/okx/factory.py +138 -0
- walrasquant/exchange/okx/oms.py +1199 -0
- walrasquant/exchange/okx/rest_api.py +799 -0
- walrasquant/exchange/okx/schema.py +1449 -0
- walrasquant/exchange/okx/websockets.py +420 -0
- walrasquant/exchange/registry.py +201 -0
- walrasquant/execution/__init__.py +24 -0
- walrasquant/execution/algorithm.py +968 -0
- walrasquant/execution/algorithms/__init__.py +3 -0
- walrasquant/execution/algorithms/twap.py +392 -0
- walrasquant/execution/config.py +34 -0
- walrasquant/execution/constants.py +27 -0
- walrasquant/execution/schema.py +62 -0
- walrasquant/indicator.py +382 -0
- walrasquant/push.py +77 -0
- walrasquant/schema.py +755 -0
- walrasquant/strategy.py +1805 -0
- walrasquant/tools/__init__.py +0 -0
- walrasquant/tools/pm2_wrapper.py +1016 -0
- walrasquant/web/__init__.py +26 -0
- walrasquant/web/app.py +157 -0
- walrasquant/web/server.py +92 -0
- walrasquant_lib-0.4.20.dist-info/METADATA +162 -0
- walrasquant_lib-0.4.20.dist-info/RECORD +105 -0
- walrasquant_lib-0.4.20.dist-info/WHEEL +4 -0
- walrasquant_lib-0.4.20.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import asyncio
|
|
3
|
+
import picows
|
|
4
|
+
from typing import Literal, Any, Callable, Dict, List
|
|
5
|
+
|
|
6
|
+
from walrasquant.base import WSClient
|
|
7
|
+
from walrasquant.exchange.okx.constants import (
|
|
8
|
+
OkxAccountType,
|
|
9
|
+
OkxKlineInterval,
|
|
10
|
+
OkxRateLimiter,
|
|
11
|
+
)
|
|
12
|
+
from walrasquant.core.entity import TaskManager
|
|
13
|
+
from walrasquant.core.nautilius_core import LiveClock, hmac_signature
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def user_pong_callback(self, frame: picows.WSFrame) -> bool:
|
|
17
|
+
self._log.debug("Pong received")
|
|
18
|
+
return (
|
|
19
|
+
frame.msg_type == picows.WSMsgType.TEXT
|
|
20
|
+
and frame.get_payload_as_memoryview() == b"pong"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class OkxWSClient(WSClient):
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
account_type: OkxAccountType,
|
|
28
|
+
handler: Callable[..., Any],
|
|
29
|
+
task_manager: TaskManager,
|
|
30
|
+
clock: LiveClock,
|
|
31
|
+
api_key: str | None = None,
|
|
32
|
+
secret: str | None = None,
|
|
33
|
+
passphrase: str | None = None,
|
|
34
|
+
business_url: bool = False,
|
|
35
|
+
custom_url: str | None = None,
|
|
36
|
+
max_subscriptions_per_client: int | None = None,
|
|
37
|
+
max_clients: int | None = None,
|
|
38
|
+
):
|
|
39
|
+
self._api_key = api_key
|
|
40
|
+
self._secret = secret
|
|
41
|
+
self._passphrase = passphrase
|
|
42
|
+
self._account_type = account_type
|
|
43
|
+
self._business_url = business_url
|
|
44
|
+
|
|
45
|
+
if custom_url:
|
|
46
|
+
url = custom_url
|
|
47
|
+
elif self.is_private:
|
|
48
|
+
if not all([self._api_key, self._passphrase, self._secret]):
|
|
49
|
+
raise ValueError("API Key, Passphrase, or Secret is missing.")
|
|
50
|
+
url = f"{account_type.stream_url}/v5/private"
|
|
51
|
+
else:
|
|
52
|
+
endpoint = "business" if business_url else "public"
|
|
53
|
+
url = f"{account_type.stream_url}/v5/{endpoint}"
|
|
54
|
+
|
|
55
|
+
super().__init__(
|
|
56
|
+
url,
|
|
57
|
+
handler=handler,
|
|
58
|
+
task_manager=task_manager,
|
|
59
|
+
clock=clock,
|
|
60
|
+
specific_ping_msg=b"ping",
|
|
61
|
+
ping_idle_timeout=5,
|
|
62
|
+
ping_reply_timeout=2,
|
|
63
|
+
user_pong_callback=user_pong_callback,
|
|
64
|
+
max_subscriptions_per_client=max_subscriptions_per_client,
|
|
65
|
+
max_clients=max_clients,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def is_private(self):
|
|
70
|
+
return (
|
|
71
|
+
self._api_key is not None
|
|
72
|
+
or self._secret is not None
|
|
73
|
+
or self._passphrase is not None
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def _get_auth_payload(self):
|
|
77
|
+
timestamp = int(self._clock.timestamp())
|
|
78
|
+
message = str(timestamp) + "GET" + "/users/self/verify"
|
|
79
|
+
digest = bytes.fromhex(hmac_signature(self._secret, message)) # type: ignore
|
|
80
|
+
sign = base64.b64encode(digest)
|
|
81
|
+
|
|
82
|
+
arg = {
|
|
83
|
+
"apiKey": self._api_key,
|
|
84
|
+
"passphrase": self._passphrase,
|
|
85
|
+
"timestamp": timestamp,
|
|
86
|
+
"sign": sign.decode("utf-8"),
|
|
87
|
+
}
|
|
88
|
+
payload = {"op": "login", "args": [arg]}
|
|
89
|
+
return payload
|
|
90
|
+
|
|
91
|
+
async def _auth(self, client_id: int | None = None):
|
|
92
|
+
self.send(self._get_auth_payload(), client_id=client_id)
|
|
93
|
+
await asyncio.sleep(5)
|
|
94
|
+
|
|
95
|
+
def _send_payload(
|
|
96
|
+
self,
|
|
97
|
+
params: List[Dict[str, Any]],
|
|
98
|
+
op: Literal["subscribe", "unsubscribe"] = "subscribe",
|
|
99
|
+
chunk_size: int = 100,
|
|
100
|
+
client_id: int | None = None,
|
|
101
|
+
):
|
|
102
|
+
# Split params into chunks of 100 if length exceeds 100
|
|
103
|
+
params_chunks = [
|
|
104
|
+
params[i : i + chunk_size] for i in range(0, len(params), chunk_size)
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
for chunk in params_chunks:
|
|
108
|
+
payload = {
|
|
109
|
+
"op": op,
|
|
110
|
+
"args": chunk,
|
|
111
|
+
}
|
|
112
|
+
self.send(payload, client_id=client_id)
|
|
113
|
+
|
|
114
|
+
def _subscribe(self, params: List[Dict[str, Any]]):
|
|
115
|
+
assigned = self._register_subscriptions(params)
|
|
116
|
+
if not assigned:
|
|
117
|
+
return
|
|
118
|
+
for client_id, client_params in assigned.items():
|
|
119
|
+
for param in client_params:
|
|
120
|
+
self._log.debug(f"Subscribing to {param}...")
|
|
121
|
+
if self._is_client_connected(client_id):
|
|
122
|
+
self._send_payload(client_params, op="subscribe", client_id=client_id)
|
|
123
|
+
|
|
124
|
+
def subscribe_funding_rate(self, symbols: List[str]):
|
|
125
|
+
params = [{"channel": "funding-rate", "instId": symbol} for symbol in symbols]
|
|
126
|
+
self._subscribe(params)
|
|
127
|
+
|
|
128
|
+
def subscribe_index_price(self, symbols: List[str]):
|
|
129
|
+
params = [{"channel": "index-tickers", "instId": symbol} for symbol in symbols]
|
|
130
|
+
self._subscribe(params)
|
|
131
|
+
|
|
132
|
+
def subscribe_mark_price(self, symbols: List[str]):
|
|
133
|
+
params = [{"channel": "mark-price", "instId": symbol} for symbol in symbols]
|
|
134
|
+
self._subscribe(params)
|
|
135
|
+
|
|
136
|
+
def subscribe_order_book(
|
|
137
|
+
self,
|
|
138
|
+
symbols: List[str],
|
|
139
|
+
channel: Literal[
|
|
140
|
+
"books", "books5", "bbo-tbt", "books-l2-tbt", "books50-l2-tbt"
|
|
141
|
+
],
|
|
142
|
+
):
|
|
143
|
+
"""
|
|
144
|
+
https://www.okx.com/docs-v5/en/#order-book-trading-market-data-ws-order-book-channel
|
|
145
|
+
"""
|
|
146
|
+
params = [{"channel": channel, "instId": symbol} for symbol in symbols]
|
|
147
|
+
self._subscribe(params)
|
|
148
|
+
|
|
149
|
+
def subscribe_trade(self, symbols: List[str]):
|
|
150
|
+
"""
|
|
151
|
+
https://www.okx.com/docs-v5/en/#order-book-trading-market-data-ws-all-trades-channel
|
|
152
|
+
"""
|
|
153
|
+
params = [{"channel": "trades", "instId": symbol} for symbol in symbols]
|
|
154
|
+
self._subscribe(params)
|
|
155
|
+
|
|
156
|
+
def subscribe_candlesticks(
|
|
157
|
+
self,
|
|
158
|
+
symbols: List[str],
|
|
159
|
+
interval: OkxKlineInterval,
|
|
160
|
+
):
|
|
161
|
+
"""
|
|
162
|
+
https://www.okx.com/docs-v5/en/#order-book-trading-market-data-ws-candlesticks-channel
|
|
163
|
+
"""
|
|
164
|
+
if not self._business_url:
|
|
165
|
+
raise ValueError("candlesticks are only supported on business url")
|
|
166
|
+
channel = interval.value
|
|
167
|
+
params = [{"channel": channel, "instId": symbol} for symbol in symbols]
|
|
168
|
+
self._subscribe(params)
|
|
169
|
+
|
|
170
|
+
def subscribe_account(self):
|
|
171
|
+
params = {"channel": "account"}
|
|
172
|
+
self._subscribe([params])
|
|
173
|
+
|
|
174
|
+
def subscribe_account_position(self):
|
|
175
|
+
params = {"channel": "balance_and_position"}
|
|
176
|
+
self._subscribe([params])
|
|
177
|
+
|
|
178
|
+
def subscribe_positions(
|
|
179
|
+
self, inst_type: Literal["MARGIN", "SWAP", "FUTURES", "OPTION", "ANY"] = "ANY"
|
|
180
|
+
):
|
|
181
|
+
params = {"channel": "positions", "instType": inst_type}
|
|
182
|
+
self._subscribe([params])
|
|
183
|
+
|
|
184
|
+
def subscribe_orders(
|
|
185
|
+
self, inst_type: Literal["MARGIN", "SWAP", "FUTURES", "OPTION", "ANY"] = "ANY"
|
|
186
|
+
):
|
|
187
|
+
params = {"channel": "orders", "instType": inst_type}
|
|
188
|
+
self._subscribe([params])
|
|
189
|
+
|
|
190
|
+
def subscribe_fills(self):
|
|
191
|
+
params = {"channel": "fills"}
|
|
192
|
+
self._subscribe([params])
|
|
193
|
+
|
|
194
|
+
def _unsubscribe(self, params: List[Dict[str, Any]]):
|
|
195
|
+
removed = self._unregister_subscriptions(params)
|
|
196
|
+
if not removed:
|
|
197
|
+
return
|
|
198
|
+
for client_id, client_params in removed.items():
|
|
199
|
+
for param in client_params:
|
|
200
|
+
self._log.debug(f"Unsubscribing from {param}...")
|
|
201
|
+
self._send_payload(client_params, op="unsubscribe", client_id=client_id)
|
|
202
|
+
|
|
203
|
+
def unsubscribe_funding_rate(self, symbols: List[str]):
|
|
204
|
+
params = [{"channel": "funding-rate", "instId": symbol} for symbol in symbols]
|
|
205
|
+
self._unsubscribe(params)
|
|
206
|
+
|
|
207
|
+
def unsubscribe_index_price(self, symbols: List[str]):
|
|
208
|
+
params = [{"channel": "index-tickers", "instId": symbol} for symbol in symbols]
|
|
209
|
+
self._unsubscribe(params)
|
|
210
|
+
|
|
211
|
+
def unsubscribe_mark_price(self, symbols: List[str]):
|
|
212
|
+
params = [{"channel": "mark-price", "instId": symbol} for symbol in symbols]
|
|
213
|
+
self._unsubscribe(params)
|
|
214
|
+
|
|
215
|
+
def unsubscribe_order_book(
|
|
216
|
+
self,
|
|
217
|
+
symbols: List[str],
|
|
218
|
+
channel: Literal[
|
|
219
|
+
"books", "books5", "bbo-tbt", "books-l2-tbt", "books50-l2-tbt"
|
|
220
|
+
],
|
|
221
|
+
):
|
|
222
|
+
params = [{"channel": channel, "instId": symbol} for symbol in symbols]
|
|
223
|
+
self._unsubscribe(params)
|
|
224
|
+
|
|
225
|
+
def unsubscribe_trade(self, symbols: List[str]):
|
|
226
|
+
params = [{"channel": "trades", "instId": symbol} for symbol in symbols]
|
|
227
|
+
self._unsubscribe(params)
|
|
228
|
+
|
|
229
|
+
def unsubscribe_candlesticks(
|
|
230
|
+
self,
|
|
231
|
+
symbols: List[str],
|
|
232
|
+
interval: OkxKlineInterval,
|
|
233
|
+
):
|
|
234
|
+
if not self._business_url:
|
|
235
|
+
raise ValueError("candlesticks are only supported on business url")
|
|
236
|
+
channel = interval.value
|
|
237
|
+
params = [{"channel": channel, "instId": symbol} for symbol in symbols]
|
|
238
|
+
self._unsubscribe(params)
|
|
239
|
+
|
|
240
|
+
def unsubscribe_account(self):
|
|
241
|
+
params = {"channel": "account"}
|
|
242
|
+
self._unsubscribe([params])
|
|
243
|
+
|
|
244
|
+
def unsubscribe_account_position(self):
|
|
245
|
+
params = {"channel": "balance_and_position"}
|
|
246
|
+
self._unsubscribe([params])
|
|
247
|
+
|
|
248
|
+
def unsubscribe_positions(
|
|
249
|
+
self, inst_type: Literal["MARGIN", "SWAP", "FUTURES", "OPTION", "ANY"] = "ANY"
|
|
250
|
+
):
|
|
251
|
+
params = {"channel": "positions", "instType": inst_type}
|
|
252
|
+
self._unsubscribe([params])
|
|
253
|
+
|
|
254
|
+
def unsubscribe_orders(
|
|
255
|
+
self, inst_type: Literal["MARGIN", "SWAP", "FUTURES", "OPTION", "ANY"] = "ANY"
|
|
256
|
+
):
|
|
257
|
+
params = {"channel": "orders", "instType": inst_type}
|
|
258
|
+
self._unsubscribe([params])
|
|
259
|
+
|
|
260
|
+
def unsubscribe_fills(self):
|
|
261
|
+
params = {"channel": "fills"}
|
|
262
|
+
self._unsubscribe([params])
|
|
263
|
+
|
|
264
|
+
async def _resubscribe_for_client(
|
|
265
|
+
self, client_id: int, subscriptions: List[Dict[str, Any]]
|
|
266
|
+
):
|
|
267
|
+
if not subscriptions:
|
|
268
|
+
return
|
|
269
|
+
if self.is_private:
|
|
270
|
+
await self._auth(client_id=client_id)
|
|
271
|
+
self._send_payload(subscriptions, client_id=client_id)
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class OkxWSApiClient(WSClient):
|
|
275
|
+
def __init__(
|
|
276
|
+
self,
|
|
277
|
+
account_type: OkxAccountType,
|
|
278
|
+
api_key: str,
|
|
279
|
+
secret: str,
|
|
280
|
+
passphrase: str,
|
|
281
|
+
handler: Callable[..., Any],
|
|
282
|
+
task_manager: TaskManager,
|
|
283
|
+
clock: LiveClock,
|
|
284
|
+
enable_rate_limit: bool,
|
|
285
|
+
):
|
|
286
|
+
self._api_key = api_key
|
|
287
|
+
self._secret = secret
|
|
288
|
+
self._passphrase = passphrase
|
|
289
|
+
self._account_type = account_type
|
|
290
|
+
|
|
291
|
+
url = f"{account_type.stream_url}/v5/private"
|
|
292
|
+
self._limiter = OkxRateLimiter(enable_rate_limit=enable_rate_limit)
|
|
293
|
+
|
|
294
|
+
super().__init__(
|
|
295
|
+
url,
|
|
296
|
+
handler=handler,
|
|
297
|
+
task_manager=task_manager,
|
|
298
|
+
clock=clock,
|
|
299
|
+
specific_ping_msg=b"ping",
|
|
300
|
+
ping_idle_timeout=5,
|
|
301
|
+
ping_reply_timeout=2,
|
|
302
|
+
user_pong_callback=user_pong_callback,
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
def _get_auth_payload(self):
|
|
306
|
+
timestamp = int(self._clock.timestamp())
|
|
307
|
+
message = str(timestamp) + "GET" + "/users/self/verify"
|
|
308
|
+
digest = bytes.fromhex(hmac_signature(self._secret, message))
|
|
309
|
+
sign = base64.b64encode(digest)
|
|
310
|
+
arg = {
|
|
311
|
+
"apiKey": self._api_key,
|
|
312
|
+
"passphrase": self._passphrase,
|
|
313
|
+
"timestamp": timestamp,
|
|
314
|
+
"sign": sign.decode("utf-8"),
|
|
315
|
+
}
|
|
316
|
+
payload = {"op": "login", "args": [arg]}
|
|
317
|
+
return payload
|
|
318
|
+
|
|
319
|
+
async def _auth(self, client_id: int | None = None):
|
|
320
|
+
self.send(self._get_auth_payload(), client_id=client_id)
|
|
321
|
+
await asyncio.sleep(5)
|
|
322
|
+
|
|
323
|
+
async def _resubscribe_for_client(self, client_id: int, subscriptions: List[Any]):
|
|
324
|
+
await self._auth(client_id=client_id)
|
|
325
|
+
|
|
326
|
+
def _submit(self, id: str, op: str, params: Dict[str, Any]):
|
|
327
|
+
payload = {
|
|
328
|
+
"id": id,
|
|
329
|
+
"op": op,
|
|
330
|
+
"args": [params],
|
|
331
|
+
}
|
|
332
|
+
self.send(payload)
|
|
333
|
+
|
|
334
|
+
async def place_order(
|
|
335
|
+
self,
|
|
336
|
+
id: str,
|
|
337
|
+
instIdCode: int,
|
|
338
|
+
tdMode: str,
|
|
339
|
+
side: str,
|
|
340
|
+
ordType: str,
|
|
341
|
+
sz: str,
|
|
342
|
+
**kwargs,
|
|
343
|
+
):
|
|
344
|
+
params = {
|
|
345
|
+
"instIdCode": instIdCode,
|
|
346
|
+
"tdMode": tdMode,
|
|
347
|
+
"side": side,
|
|
348
|
+
"ordType": ordType,
|
|
349
|
+
"sz": sz,
|
|
350
|
+
**kwargs,
|
|
351
|
+
}
|
|
352
|
+
await self._limiter.order_limit("/ws/order")
|
|
353
|
+
self._submit(id, "order", params)
|
|
354
|
+
|
|
355
|
+
async def cancel_order(self, id: str, instIdCode: int, clOrdId: str):
|
|
356
|
+
params = {
|
|
357
|
+
"instIdCode": instIdCode,
|
|
358
|
+
"clOrdId": clOrdId,
|
|
359
|
+
}
|
|
360
|
+
# await self._limiter.order_limit("/ws/cancel")
|
|
361
|
+
self._submit(id, "cancel-order", params)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# import asyncio # noqa
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
# async def main():
|
|
368
|
+
# from walrasquant.constants import settings
|
|
369
|
+
# from walrasquant.core.entity import TaskManager
|
|
370
|
+
# from walrasquant.core.nautilius_core import LiveClock, setup_nexus_core, UUID4
|
|
371
|
+
|
|
372
|
+
# OKX_API_KEY = settings.OKX.DEMO_1.API_KEY
|
|
373
|
+
# OKX_SECRET = settings.OKX.DEMO_1.SECRET
|
|
374
|
+
# OKX_PASSPHRASE = settings.OKX.DEMO_1.PASSPHRASE
|
|
375
|
+
|
|
376
|
+
# log_guard = setup_nexus_core( # noqa
|
|
377
|
+
# trader_id="bnc-test",
|
|
378
|
+
# level_stdout="DEBUG",
|
|
379
|
+
# )
|
|
380
|
+
|
|
381
|
+
# task_manager = TaskManager(
|
|
382
|
+
# loop=asyncio.get_event_loop(),
|
|
383
|
+
# )
|
|
384
|
+
|
|
385
|
+
# ws_api_client = OkxWSApiClient(
|
|
386
|
+
# account_type=OkxAccountType.DEMO,
|
|
387
|
+
# api_key=OKX_API_KEY,
|
|
388
|
+
# secret=OKX_SECRET,
|
|
389
|
+
# passphrase=OKX_PASSPHRASE,
|
|
390
|
+
# handler=lambda msg: print(msg),
|
|
391
|
+
# task_manager=task_manager,
|
|
392
|
+
# clock=LiveClock(),
|
|
393
|
+
# enable_rate_limit=True,
|
|
394
|
+
# )
|
|
395
|
+
|
|
396
|
+
# await ws_api_client.connect()
|
|
397
|
+
# # await ws_api_client.subscribe_orders()
|
|
398
|
+
|
|
399
|
+
# # await ws_api_client.place_order(
|
|
400
|
+
# # id=strip_uuid_hyphens(UUID4().value),
|
|
401
|
+
# # instId="BTC-USDT-SWAP",
|
|
402
|
+
# # tdMode="cross",
|
|
403
|
+
# # side="buy",
|
|
404
|
+
# # ordType="limit",
|
|
405
|
+
# # sz="0.1",
|
|
406
|
+
# # px="100000"
|
|
407
|
+
# # # timeInForce="GTC",
|
|
408
|
+
# # )
|
|
409
|
+
|
|
410
|
+
# await ws_api_client.cancel_order(
|
|
411
|
+
# id="ffab98098c664e7c847290192015089d",
|
|
412
|
+
# instId="BTC-USDT-SWAP",
|
|
413
|
+
# ordId="2791773453976276992",
|
|
414
|
+
# )
|
|
415
|
+
|
|
416
|
+
# await task_manager.wait()
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
# if __name__ == "__main__":
|
|
420
|
+
# asyncio.run(main())
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Factory registry for exchange components.
|
|
3
|
+
|
|
4
|
+
This module provides a centralized registry for exchange factories with
|
|
5
|
+
auto-discovery capabilities.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import importlib
|
|
9
|
+
import pkgutil
|
|
10
|
+
from typing import Dict, List
|
|
11
|
+
from walrasquant.constants import ExchangeType
|
|
12
|
+
from walrasquant.exchange.base_factory import ExchangeFactory
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ExchangeRegistry:
|
|
16
|
+
"""Registry for exchange factories with auto-discovery."""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self._factories: Dict[ExchangeType, ExchangeFactory] = {}
|
|
20
|
+
self._auto_discovered = False
|
|
21
|
+
|
|
22
|
+
def register(self, factory: ExchangeFactory) -> None:
|
|
23
|
+
"""
|
|
24
|
+
Register an exchange factory.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
factory: Factory instance to register
|
|
28
|
+
|
|
29
|
+
Raises:
|
|
30
|
+
ValueError: If exchange type is already registered
|
|
31
|
+
"""
|
|
32
|
+
exchange_type = factory.exchange_type
|
|
33
|
+
if exchange_type in self._factories:
|
|
34
|
+
raise ValueError(f"Exchange {exchange_type} is already registered")
|
|
35
|
+
|
|
36
|
+
self._factories[exchange_type] = factory
|
|
37
|
+
|
|
38
|
+
def get(self, exchange_type: ExchangeType) -> ExchangeFactory:
|
|
39
|
+
"""
|
|
40
|
+
Get factory for exchange type.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
exchange_type: Type of exchange
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Factory instance
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
ValueError: If exchange type is not registered
|
|
50
|
+
"""
|
|
51
|
+
# Auto-discover if not done yet
|
|
52
|
+
if not self._auto_discovered:
|
|
53
|
+
self.discover_factories()
|
|
54
|
+
|
|
55
|
+
if exchange_type not in self._factories:
|
|
56
|
+
raise ValueError(f"Exchange {exchange_type} is not registered")
|
|
57
|
+
|
|
58
|
+
return self._factories[exchange_type]
|
|
59
|
+
|
|
60
|
+
def list_exchanges(self) -> List[ExchangeType]:
|
|
61
|
+
"""
|
|
62
|
+
List all registered exchange types.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
List of registered exchange types
|
|
66
|
+
"""
|
|
67
|
+
if not self._auto_discovered:
|
|
68
|
+
self.discover_factories()
|
|
69
|
+
|
|
70
|
+
return list(self._factories.keys())
|
|
71
|
+
|
|
72
|
+
def is_registered(self, exchange_type: ExchangeType) -> bool:
|
|
73
|
+
"""
|
|
74
|
+
Check if exchange type is registered.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
exchange_type: Type of exchange to check
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
True if registered, False otherwise
|
|
81
|
+
"""
|
|
82
|
+
if not self._auto_discovered:
|
|
83
|
+
self.discover_factories()
|
|
84
|
+
|
|
85
|
+
return exchange_type in self._factories
|
|
86
|
+
|
|
87
|
+
def discover_factories(self) -> None:
|
|
88
|
+
"""
|
|
89
|
+
Auto-discover and register exchange factories.
|
|
90
|
+
|
|
91
|
+
This method scans the walrasquant.exchange package for factory modules
|
|
92
|
+
and automatically registers them.
|
|
93
|
+
"""
|
|
94
|
+
if self._auto_discovered:
|
|
95
|
+
return
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
import walrasquant.exchange as exchange_package
|
|
99
|
+
|
|
100
|
+
# Get the path of the exchange package
|
|
101
|
+
package_path = exchange_package.__path__
|
|
102
|
+
|
|
103
|
+
# Scan for subpackages (exchange implementations)
|
|
104
|
+
for importer, modname, ispkg in pkgutil.iter_modules(package_path):
|
|
105
|
+
if not ispkg or modname in ("__pycache__", "base_factory", "build"):
|
|
106
|
+
continue
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
# Try to import the factory module
|
|
110
|
+
factory_module_name = f"walrasquant.exchange.{modname}.factory"
|
|
111
|
+
factory_module = importlib.import_module(factory_module_name)
|
|
112
|
+
|
|
113
|
+
# Look for factory classes
|
|
114
|
+
for attr_name in dir(factory_module):
|
|
115
|
+
attr = getattr(factory_module, attr_name)
|
|
116
|
+
|
|
117
|
+
# Check if it's a factory class (not the base class)
|
|
118
|
+
if (
|
|
119
|
+
isinstance(attr, type)
|
|
120
|
+
and issubclass(attr, ExchangeFactory)
|
|
121
|
+
and attr is not ExchangeFactory
|
|
122
|
+
):
|
|
123
|
+
# Instantiate and register
|
|
124
|
+
factory_instance = attr()
|
|
125
|
+
if factory_instance.exchange_type not in self._factories:
|
|
126
|
+
self._factories[factory_instance.exchange_type] = (
|
|
127
|
+
factory_instance
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
except ImportError:
|
|
131
|
+
# Skip exchanges that don't have factory modules
|
|
132
|
+
continue
|
|
133
|
+
except Exception as e:
|
|
134
|
+
# Log but don't fail on individual exchange errors
|
|
135
|
+
print(f"Warning: Failed to register factory for {modname}: {e}")
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
except Exception as e:
|
|
139
|
+
print(f"Warning: Factory auto-discovery failed: {e}")
|
|
140
|
+
|
|
141
|
+
self._auto_discovered = True
|
|
142
|
+
|
|
143
|
+
def clear(self) -> None:
|
|
144
|
+
"""Clear all registered factories (mainly for testing)."""
|
|
145
|
+
self._factories.clear()
|
|
146
|
+
self._auto_discovered = False
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# Global registry instance
|
|
150
|
+
_registry = ExchangeRegistry()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def register_factory(factory: ExchangeFactory) -> None:
|
|
154
|
+
"""
|
|
155
|
+
Register an exchange factory in the global registry.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
factory: Factory instance to register
|
|
159
|
+
"""
|
|
160
|
+
_registry.register(factory)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def get_factory(exchange_type: ExchangeType) -> ExchangeFactory:
|
|
164
|
+
"""
|
|
165
|
+
Get factory for exchange type from the global registry.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
exchange_type: Type of exchange
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Factory instance
|
|
172
|
+
"""
|
|
173
|
+
return _registry.get(exchange_type)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def list_exchanges() -> List[ExchangeType]:
|
|
177
|
+
"""
|
|
178
|
+
List all registered exchange types.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
List of registered exchange types
|
|
182
|
+
"""
|
|
183
|
+
return _registry.list_exchanges()
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def is_exchange_registered(exchange_type: ExchangeType) -> bool:
|
|
187
|
+
"""
|
|
188
|
+
Check if exchange type is registered.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
exchange_type: Type of exchange to check
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
True if registered, False otherwise
|
|
195
|
+
"""
|
|
196
|
+
return _registry.is_registered(exchange_type)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def discover_exchanges() -> None:
|
|
200
|
+
"""Force discovery of exchange factories."""
|
|
201
|
+
_registry.discover_factories()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from walrasquant.execution.algorithm import ExecAlgorithm
|
|
2
|
+
from walrasquant.execution.config import ExecAlgorithmConfig, TWAPConfig
|
|
3
|
+
from walrasquant.execution.schema import (
|
|
4
|
+
ExecAlgorithmCommand,
|
|
5
|
+
ExecAlgorithmOrder,
|
|
6
|
+
ExecAlgorithmOrderParams,
|
|
7
|
+
)
|
|
8
|
+
from walrasquant.execution.constants import (
|
|
9
|
+
ExecAlgorithmStatus,
|
|
10
|
+
ExecAlgorithmCommandType,
|
|
11
|
+
)
|
|
12
|
+
from walrasquant.execution.algorithms import TWAPExecAlgorithm
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ExecAlgorithm",
|
|
16
|
+
"ExecAlgorithmCommandType",
|
|
17
|
+
"ExecAlgorithmCommand",
|
|
18
|
+
"ExecAlgorithmOrder",
|
|
19
|
+
"ExecAlgorithmOrderParams",
|
|
20
|
+
"ExecAlgorithmStatus",
|
|
21
|
+
"ExecAlgorithmConfig",
|
|
22
|
+
"TWAPConfig",
|
|
23
|
+
"TWAPExecAlgorithm",
|
|
24
|
+
]
|