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.
Files changed (105) hide show
  1. walrasquant/__init__.py +7 -0
  2. walrasquant/aggregation.py +449 -0
  3. walrasquant/backends/__init__.py +5 -0
  4. walrasquant/backends/db.py +109 -0
  5. walrasquant/backends/db_memory.py +61 -0
  6. walrasquant/backends/db_postgresql.py +321 -0
  7. walrasquant/backends/db_sqlite.py +310 -0
  8. walrasquant/base/__init__.py +24 -0
  9. walrasquant/base/api_client.py +46 -0
  10. walrasquant/base/connector.py +863 -0
  11. walrasquant/base/ems.py +794 -0
  12. walrasquant/base/exchange.py +213 -0
  13. walrasquant/base/oms.py +428 -0
  14. walrasquant/base/retry.py +220 -0
  15. walrasquant/base/sms.py +545 -0
  16. walrasquant/base/ws_client.py +408 -0
  17. walrasquant/config.py +284 -0
  18. walrasquant/constants.py +413 -0
  19. walrasquant/core/__init__.py +0 -0
  20. walrasquant/core/cache.py +688 -0
  21. walrasquant/core/clock.py +59 -0
  22. walrasquant/core/connection.py +41 -0
  23. walrasquant/core/entity.py +504 -0
  24. walrasquant/core/nautilius_core.py +103 -0
  25. walrasquant/core/registry.py +41 -0
  26. walrasquant/engine.py +745 -0
  27. walrasquant/error.py +34 -0
  28. walrasquant/exchange/__init__.py +13 -0
  29. walrasquant/exchange/base_factory.py +172 -0
  30. walrasquant/exchange/binance/__init__.py +30 -0
  31. walrasquant/exchange/binance/connector.py +1093 -0
  32. walrasquant/exchange/binance/constants.py +934 -0
  33. walrasquant/exchange/binance/ems.py +140 -0
  34. walrasquant/exchange/binance/error.py +48 -0
  35. walrasquant/exchange/binance/exchange.py +144 -0
  36. walrasquant/exchange/binance/factory.py +115 -0
  37. walrasquant/exchange/binance/oms.py +1807 -0
  38. walrasquant/exchange/binance/rest_api.py +1653 -0
  39. walrasquant/exchange/binance/schema.py +1063 -0
  40. walrasquant/exchange/binance/websockets.py +389 -0
  41. walrasquant/exchange/bitget/__init__.py +28 -0
  42. walrasquant/exchange/bitget/connector.py +578 -0
  43. walrasquant/exchange/bitget/constants.py +392 -0
  44. walrasquant/exchange/bitget/ems.py +202 -0
  45. walrasquant/exchange/bitget/error.py +36 -0
  46. walrasquant/exchange/bitget/exchange.py +128 -0
  47. walrasquant/exchange/bitget/factory.py +135 -0
  48. walrasquant/exchange/bitget/oms.py +1619 -0
  49. walrasquant/exchange/bitget/rest_api.py +610 -0
  50. walrasquant/exchange/bitget/schema.py +885 -0
  51. walrasquant/exchange/bitget/websockets.py +753 -0
  52. walrasquant/exchange/bybit/__init__.py +32 -0
  53. walrasquant/exchange/bybit/connector.py +819 -0
  54. walrasquant/exchange/bybit/constants.py +479 -0
  55. walrasquant/exchange/bybit/ems.py +93 -0
  56. walrasquant/exchange/bybit/error.py +36 -0
  57. walrasquant/exchange/bybit/exchange.py +108 -0
  58. walrasquant/exchange/bybit/factory.py +128 -0
  59. walrasquant/exchange/bybit/oms.py +1195 -0
  60. walrasquant/exchange/bybit/rest_api.py +570 -0
  61. walrasquant/exchange/bybit/schema.py +867 -0
  62. walrasquant/exchange/bybit/websockets.py +307 -0
  63. walrasquant/exchange/hyperliquid/__init__.py +28 -0
  64. walrasquant/exchange/hyperliquid/connector.py +370 -0
  65. walrasquant/exchange/hyperliquid/constants.py +371 -0
  66. walrasquant/exchange/hyperliquid/ems.py +156 -0
  67. walrasquant/exchange/hyperliquid/error.py +48 -0
  68. walrasquant/exchange/hyperliquid/exchange.py +120 -0
  69. walrasquant/exchange/hyperliquid/factory.py +135 -0
  70. walrasquant/exchange/hyperliquid/oms.py +1081 -0
  71. walrasquant/exchange/hyperliquid/rest_api.py +348 -0
  72. walrasquant/exchange/hyperliquid/schema.py +583 -0
  73. walrasquant/exchange/hyperliquid/websockets.py +592 -0
  74. walrasquant/exchange/okx/__init__.py +25 -0
  75. walrasquant/exchange/okx/connector.py +931 -0
  76. walrasquant/exchange/okx/constants.py +518 -0
  77. walrasquant/exchange/okx/ems.py +144 -0
  78. walrasquant/exchange/okx/error.py +66 -0
  79. walrasquant/exchange/okx/exchange.py +102 -0
  80. walrasquant/exchange/okx/factory.py +138 -0
  81. walrasquant/exchange/okx/oms.py +1199 -0
  82. walrasquant/exchange/okx/rest_api.py +799 -0
  83. walrasquant/exchange/okx/schema.py +1449 -0
  84. walrasquant/exchange/okx/websockets.py +420 -0
  85. walrasquant/exchange/registry.py +201 -0
  86. walrasquant/execution/__init__.py +24 -0
  87. walrasquant/execution/algorithm.py +968 -0
  88. walrasquant/execution/algorithms/__init__.py +3 -0
  89. walrasquant/execution/algorithms/twap.py +392 -0
  90. walrasquant/execution/config.py +34 -0
  91. walrasquant/execution/constants.py +27 -0
  92. walrasquant/execution/schema.py +62 -0
  93. walrasquant/indicator.py +382 -0
  94. walrasquant/push.py +77 -0
  95. walrasquant/schema.py +755 -0
  96. walrasquant/strategy.py +1805 -0
  97. walrasquant/tools/__init__.py +0 -0
  98. walrasquant/tools/pm2_wrapper.py +1016 -0
  99. walrasquant/web/__init__.py +26 -0
  100. walrasquant/web/app.py +157 -0
  101. walrasquant/web/server.py +92 -0
  102. walrasquant_lib-0.4.20.dist-info/METADATA +162 -0
  103. walrasquant_lib-0.4.20.dist-info/RECORD +105 -0
  104. walrasquant_lib-0.4.20.dist-info/WHEEL +4 -0
  105. 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
+ ]