walrasquant-lib 0.4.23__tar.gz → 0.4.24__tar.gz

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_lib-0.4.23 → walrasquant_lib-0.4.24}/PKG-INFO +2 -2
  2. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/pyproject.toml +2 -2
  3. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/base/api_client.py +10 -0
  4. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/base/ems.py +52 -327
  5. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/config.py +3 -0
  6. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/engine.py +12 -1
  7. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/base_factory.py +1 -0
  8. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/binance/connector.py +84 -51
  9. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/binance/ems.py +2 -0
  10. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/binance/factory.py +1 -0
  11. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/binance/schema.py +25 -10
  12. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/bitget/ems.py +2 -0
  13. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/bitget/factory.py +1 -0
  14. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/bybit/connector.py +21 -12
  15. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/bybit/ems.py +2 -0
  16. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/bybit/factory.py +1 -0
  17. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/bybit/schema.py +12 -11
  18. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/hyperliquid/ems.py +2 -0
  19. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/hyperliquid/factory.py +1 -0
  20. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/hyperliquid/rest_api.py +3 -53
  21. walrasquant_lib-0.4.24/src/walrasquant/exchange/hyperliquid/signing.py +72 -0
  22. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/hyperliquid/websockets.py +3 -53
  23. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/okx/connector.py +27 -18
  24. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/okx/ems.py +2 -0
  25. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/okx/factory.py +1 -0
  26. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/okx/schema.py +0 -2
  27. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/schema.py +1 -1
  28. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/README.md +0 -0
  29. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/__init__.py +0 -0
  30. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/aggregation.py +0 -0
  31. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/backends/__init__.py +0 -0
  32. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/backends/db.py +0 -0
  33. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/backends/db_memory.py +0 -0
  34. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/backends/db_postgresql.py +0 -0
  35. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/backends/db_sqlite.py +0 -0
  36. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/base/__init__.py +0 -0
  37. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/base/connector.py +0 -0
  38. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/base/exchange.py +0 -0
  39. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/base/oms.py +0 -0
  40. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/base/retry.py +0 -0
  41. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/base/sms.py +0 -0
  42. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/base/ws_client.py +0 -0
  43. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/constants.py +0 -0
  44. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/core/__init__.py +0 -0
  45. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/core/cache.py +0 -0
  46. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/core/clock.py +0 -0
  47. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/core/connection.py +0 -0
  48. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/core/entity.py +0 -0
  49. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/core/nautilius_core.py +0 -0
  50. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/core/registry.py +0 -0
  51. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/error.py +0 -0
  52. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/__init__.py +0 -0
  53. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/binance/__init__.py +0 -0
  54. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/binance/constants.py +0 -0
  55. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/binance/error.py +0 -0
  56. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/binance/exchange.py +0 -0
  57. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/binance/oms.py +0 -0
  58. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/binance/rest_api.py +0 -0
  59. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/binance/websockets.py +0 -0
  60. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/bitget/__init__.py +0 -0
  61. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/bitget/connector.py +0 -0
  62. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/bitget/constants.py +0 -0
  63. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/bitget/error.py +0 -0
  64. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/bitget/exchange.py +0 -0
  65. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/bitget/oms.py +0 -0
  66. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/bitget/rest_api.py +0 -0
  67. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/bitget/schema.py +0 -0
  68. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/bitget/websockets.py +0 -0
  69. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/bybit/__init__.py +0 -0
  70. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/bybit/constants.py +0 -0
  71. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/bybit/error.py +0 -0
  72. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/bybit/exchange.py +0 -0
  73. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/bybit/oms.py +0 -0
  74. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/bybit/rest_api.py +0 -0
  75. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/bybit/websockets.py +0 -0
  76. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/hyperliquid/__init__.py +0 -0
  77. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/hyperliquid/connector.py +0 -0
  78. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/hyperliquid/constants.py +0 -0
  79. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/hyperliquid/error.py +0 -0
  80. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/hyperliquid/exchange.py +0 -0
  81. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/hyperliquid/oms.py +0 -0
  82. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/hyperliquid/schema.py +0 -0
  83. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/okx/__init__.py +0 -0
  84. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/okx/constants.py +0 -0
  85. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/okx/error.py +0 -0
  86. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/okx/exchange.py +0 -0
  87. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/okx/oms.py +0 -0
  88. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/okx/rest_api.py +0 -0
  89. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/okx/websockets.py +0 -0
  90. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/exchange/registry.py +0 -0
  91. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/execution/__init__.py +0 -0
  92. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/execution/algorithm.py +0 -0
  93. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/execution/algorithms/__init__.py +0 -0
  94. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/execution/algorithms/twap.py +0 -0
  95. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/execution/config.py +0 -0
  96. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/execution/constants.py +0 -0
  97. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/execution/schema.py +0 -0
  98. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/indicator.py +0 -0
  99. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/push.py +0 -0
  100. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/strategy.py +0 -0
  101. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/tools/__init__.py +0 -0
  102. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/tools/pm2_wrapper.py +0 -0
  103. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/web/__init__.py +0 -0
  104. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/web/app.py +0 -0
  105. {walrasquant_lib-0.4.23 → walrasquant_lib-0.4.24}/src/walrasquant/web/server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: walrasquant-lib
3
- Version: 0.4.23
3
+ Version: 0.4.24
4
4
  Summary: fastest python trading bot
5
5
  Author: River-Shi
6
6
  Author-email: River-Shi <nachuan.shi.quant@gmail.com>
@@ -23,13 +23,13 @@ Requires-Dist: dynaconf[redis]>=3.2.12
23
23
  Requires-Dist: aiohttp>=3.13.3
24
24
  Requires-Dist: rich>=14.0.0
25
25
  Requires-Dist: picows>=2.1.1
26
- Requires-Dist: ccxt>=4.5.63
27
26
  Requires-Dist: nexuscore-lib>=0.2.0
28
27
  Requires-Dist: flashduty-lib>=0.2.0
29
28
  Requires-Dist: nexuslog-lib>=0.5.0
30
29
  Requires-Dist: msgspec>=0.21.1
31
30
  Requires-Dist: pandas<3.0.0
32
31
  Requires-Dist: numpy>=2.2.0
32
+ Requires-Dist: ccxt>=4.5.64
33
33
  Requires-Python: >=3.11, <3.15
34
34
  Description-Content-Type: text/markdown
35
35
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "walrasquant-lib"
3
- version = "0.4.23"
3
+ version = "0.4.24"
4
4
  description = "fastest python trading bot"
5
5
  authors = [
6
6
  {name = "River-Shi",email = "nachuan.shi.quant@gmail.com"}
@@ -27,13 +27,13 @@ dependencies = [
27
27
  "aiohttp>=3.13.3",
28
28
  "rich>=14.0.0",
29
29
  "picows>=2.1.1",
30
- "ccxt>=4.5.63",
31
30
  "nexuscore-lib>=0.2.0",
32
31
  "flashduty-lib>=0.2.0",
33
32
  "nexuslog-lib>=0.5.0",
34
33
  "msgspec>=0.21.1",
35
34
  "pandas<3.0.0",
36
35
  "numpy>=2.2.0",
36
+ "ccxt>=4.5.64",
37
37
  ]
38
38
 
39
39
  [project.scripts]
@@ -29,8 +29,18 @@ class ApiClient(ABC):
29
29
 
30
30
  async def _init_session(self, base_url: str | None = None):
31
31
  if self._session is None:
32
+ # The default TCPConnector drops idle connections after 15s and
33
+ # resolves DNS on a thread pool, so intermittent traders pay a
34
+ # fresh DNS + TCP + TLS handshake on the order path.
35
+ connector = aiohttp.TCPConnector(
36
+ limit=64,
37
+ ttl_dns_cache=300,
38
+ keepalive_timeout=60,
39
+ resolver=aiohttp.AsyncResolver(),
40
+ )
32
41
  kwargs: Dict[str, Any] = {
33
42
  "timeout": aiohttp.ClientTimeout(total=self._timeout),
43
+ "connector": connector,
34
44
  }
35
45
  if base_url:
36
46
  kwargs["base_url"] = base_url
@@ -1,7 +1,7 @@
1
1
  import asyncio
2
2
  import traceback
3
3
  from abc import ABC, abstractmethod
4
- from typing import Any, Mapping, List, Dict, cast
4
+ from typing import Any, Mapping, List, Dict, cast, Tuple
5
5
  from typing import Literal
6
6
  from decimal import Decimal
7
7
  import nexuslog as logging
@@ -39,7 +39,7 @@ from walrasquant.base.connector import PrivateConnector
39
39
  _CREATE_SUBMIT_TYPES: frozenset = frozenset({SubmitType.CREATE, SubmitType.CREATE_WS})
40
40
 
41
41
 
42
- def _item_priority(item: tuple) -> int:
42
+ def _item_priority(item: Tuple[OrderSubmit, SubmitType]) -> int:
43
43
  """Return the scheduling priority for a queue item.
44
44
 
45
45
  Priority scale (lower = higher priority):
@@ -89,9 +89,6 @@ class PriorityOrderQueue:
89
89
  _, _, item = await self._queue.get()
90
90
  return item
91
91
 
92
- def task_done(self) -> None:
93
- self._queue.task_done()
94
-
95
92
 
96
93
  class ExecutionManagementSystem(ABC):
97
94
  _BATCH_CHUNK_SIZE: int = 20
@@ -105,6 +102,7 @@ class ExecutionManagementSystem(ABC):
105
102
  task_manager: TaskManager,
106
103
  registry: OrderRegistry,
107
104
  queue_maxsize: int = 100_000,
105
+ concurrency: int = 3,
108
106
  ):
109
107
  self._log = logging.getLogger(name=type(self).__name__)
110
108
 
@@ -115,9 +113,10 @@ class ExecutionManagementSystem(ABC):
115
113
  self._registry = registry
116
114
  self._clock = clock
117
115
  self._queue_maxsize = queue_maxsize
116
+ self._concurrency = concurrency
118
117
  self._order_submit_queues: Dict[AccountType, PriorityOrderQueue] = {}
119
118
  self._private_connectors: Dict[AccountType, PrivateConnector] = {}
120
- self._pending_create_oids: set[str] = set()
119
+ self._shutdown: bool = False
121
120
 
122
121
  def _safe_put(self, queue: PriorityOrderQueue, item: Any) -> None:
123
122
  # Order submits may originate off the event loop (e.g. threaded scheduled
@@ -138,15 +137,11 @@ class ExecutionManagementSystem(ABC):
138
137
 
139
138
  def _do_put(self, queue: PriorityOrderQueue, item: Any) -> None:
140
139
  # Always runs on the loop thread: queue access + bookkeeping are serialized.
141
- order_submit, submit_type = item
140
+ if self._shutdown:
141
+ self._log.warning(f"EMS is shutting down; order submit rejected: {item}")
142
+ return
142
143
  priority = _item_priority(item)
143
144
 
144
- if submit_type in _CREATE_SUBMIT_TYPES:
145
- self._pending_create_oids.add(order_submit.oid)
146
- elif submit_type in (SubmitType.CANCEL, SubmitType.CANCEL_WS):
147
- if order_submit.oid in self._pending_create_oids:
148
- priority = 6 # demote: paired create must execute first
149
-
150
145
  if queue.maxsize == 0 or queue.qsize() < queue.maxsize:
151
146
  try:
152
147
  queue.put_nowait(item, priority=priority)
@@ -323,7 +318,6 @@ class ExecutionManagementSystem(ABC):
323
318
  Cancel an order
324
319
  """
325
320
  # self._cache.mark_cancel_intent(order_submit.oid)
326
- self._pending_create_oids.discard(order_submit.oid)
327
321
  await self._private_connectors[account_type]._oms.cancel_order(
328
322
  oid=order_submit.oid,
329
323
  symbol=order_submit.symbol,
@@ -337,7 +331,6 @@ class ExecutionManagementSystem(ABC):
337
331
  Cancel an order
338
332
  """
339
333
  # self._cache.mark_cancel_intent(order_submit.oid)
340
- self._pending_create_oids.discard(order_submit.oid)
341
334
  await self._private_connectors[account_type]._oms.cancel_order_ws(
342
335
  oid=order_submit.oid,
343
336
  symbol=order_submit.symbol,
@@ -350,7 +343,6 @@ class ExecutionManagementSystem(ABC):
350
343
  """
351
344
  Create an order
352
345
  """
353
- self._pending_create_oids.discard(order_submit.oid)
354
346
  self._registry.register_order(order_submit.oid)
355
347
  self._cache.add_inflight_order(order_submit.symbol, order_submit.oid)
356
348
  await self._private_connectors[account_type]._oms.create_order(
@@ -371,7 +363,6 @@ class ExecutionManagementSystem(ABC):
371
363
  """
372
364
  Create an order
373
365
  """
374
- self._pending_create_oids.discard(order_submit.oid)
375
366
  self._registry.register_order(order_submit.oid)
376
367
  self._cache.add_inflight_order(order_submit.symbol, order_submit.oid)
377
368
  await self._private_connectors[account_type]._oms.create_order_ws(
@@ -404,293 +395,6 @@ class ExecutionManagementSystem(ABC):
404
395
  """
405
396
  pass
406
397
 
407
- # async def _auto_maker(self, order_submit: OrderSubmit, account_type: AccountType):
408
- # """
409
- # Auto maker order: always place the order at the best price
410
- # """
411
- # pass
412
-
413
- # def _calculate_twap_orders(
414
- # self,
415
- # symbol: str,
416
- # total_amount: Decimal,
417
- # duration: float,
418
- # wait: float,
419
- # min_order_amount: Decimal,
420
- # reduce_only: bool = False,
421
- # ) -> Tuple[List[Decimal], float]:
422
- # """
423
- # Calculate the amount list and wait time for the twap order
424
-
425
- # eg:
426
- # amount_list = [10, 10, 10]
427
- # wait = 10
428
- # """
429
- # amount_list = []
430
- # if total_amount == 0 or total_amount < min_order_amount:
431
- # if reduce_only and total_amount > 0:
432
- # self._log.info(
433
- # f"TWAP ORDER: {symbol} Total amount is less than min order amount: {total_amount} < {min_order_amount}, reduce_only: {reduce_only}"
434
- # )
435
- # return [total_amount], 0
436
- # self._log.info(
437
- # f"TWAP ORDER: {symbol} Total amount is less than min order amount: {total_amount} < {min_order_amount}"
438
- # )
439
- # return [], 0
440
-
441
- # interval = duration // wait
442
- # base_amount = float(total_amount) / interval
443
-
444
- # base_amount = max(
445
- # min_order_amount, self._amount_to_precision(symbol, base_amount)
446
- # )
447
-
448
- # interval = int(total_amount // base_amount)
449
- # remaining = total_amount - interval * base_amount
450
-
451
- # amount_list = [base_amount] * interval
452
-
453
- # if remaining >= min_order_amount or (reduce_only and remaining > 0):
454
- # amount_list.append(remaining)
455
- # else:
456
- # amount_list[-1] += remaining
457
-
458
- # wait = duration / len(amount_list)
459
-
460
- # self._log.info(f"TWAP ORDER: {symbol} Amount list: {amount_list}, Wait: {wait}")
461
-
462
- # return amount_list, wait
463
-
464
- # def _cal_limit_order_price(
465
- # self, symbol: str, side: OrderSide, market: BaseMarket
466
- # ) -> Decimal:
467
- # """
468
- # Calculate the limit order price
469
- # """
470
- # basis_point = market.precision.price
471
- # book = self._cache.bookl1(symbol)
472
-
473
- # if side.is_buy:
474
- # # if the spread is greater than the basis point
475
- # if book.spread > basis_point:
476
- # price = book.ask - basis_point
477
- # else:
478
- # price = book.bid
479
- # elif side.is_sell:
480
- # # if the spread is greater than the basis point
481
- # if book.spread > basis_point:
482
- # price = book.bid + basis_point
483
- # else:
484
- # price = book.ask
485
- # price = self._price_to_precision(symbol, price)
486
- # self._log.debug(
487
- # f"CALCULATE LIMIT ORDER PRICE: symbol: {symbol}, side: {side}, price: {price}, ask: {book.ask}, bid: {book.bid}"
488
- # )
489
- # return price
490
-
491
- # NOTE: TWAP order related code is commented out for now
492
- # async def _twap_order(
493
- # self, order_submit: TWAPOrderSubmit, account_type: AccountType
494
- # ):
495
- # """
496
- # Execute the twap order
497
- # """
498
- # symbol = order_submit.symbol
499
- # instrument_id = order_submit.instrument_id
500
- # side = order_submit.side
501
- # market = self._market[symbol]
502
- # position_side = order_submit.position_side
503
- # kwargs = order_submit.kwargs
504
- # twap_uuid = order_submit.uuid
505
- # check_interval = order_submit.check_interval
506
- # reduce_only = order_submit.kwargs.get("reduce_only", False)
507
-
508
- # algo_order = AlgoOrder(
509
- # symbol=symbol,
510
- # uuid=twap_uuid,
511
- # side=side,
512
- # amount=order_submit.amount,
513
- # duration=order_submit.duration,
514
- # wait=order_submit.wait,
515
- # status=AlgoOrderStatus.RUNNING,
516
- # exchange=instrument_id.exchange,
517
- # timestamp=self._clock.timestamp_ms(),
518
- # position_side=position_side,
519
- # )
520
-
521
- # self._cache._order_initialized(algo_order)
522
-
523
- # min_order_amount: Decimal = self._get_min_order_amount(symbol, market)
524
- # amount_list, wait = self._calculate_twap_orders(
525
- # symbol=symbol,
526
- # total_amount=order_submit.amount,
527
- # duration=order_submit.duration,
528
- # wait=order_submit.wait,
529
- # min_order_amount=min_order_amount,
530
- # reduce_only=reduce_only,
531
- # )
532
-
533
- # order_id = None
534
- # elapsed_time = 0
535
-
536
- # try:
537
- # while amount_list:
538
- # if order_id:
539
- # order = self._cache.get_order(order_id)
540
-
541
- # is_opened = order.bind_optional(
542
- # lambda order: order.is_opened
543
- # ).value_or(False)
544
- # on_flight = order.bind_optional(
545
- # lambda order: order.on_flight
546
- # ).value_or(False)
547
- # is_closed = order.bind_optional(
548
- # lambda order: order.is_closed
549
- # ).value_or(False)
550
-
551
- # # 检查现价单是否已成交,不然的话立刻下市价单成交 或者 把remaining amount加到下一个市价单上
552
- # if is_opened and not on_flight:
553
- # await self._cancel_order(
554
- # order_submit=CancelOrderSubmit(
555
- # symbol=symbol,
556
- # instrument_id=instrument_id,
557
- # submit_type=SubmitType.CANCEL,
558
- # uuid=order_id,
559
- # ),
560
- # account_type=account_type,
561
- # )
562
- # elif is_closed:
563
- # order_id = None
564
- # remaining = order.unwrap().remaining
565
- # if remaining >= min_order_amount or (
566
- # reduce_only and remaining > 0
567
- # ):
568
- # order = await self._create_order(
569
- # order_submit=CreateOrderSubmit(
570
- # symbol=symbol,
571
- # instrument_id=instrument_id,
572
- # submit_type=SubmitType.CREATE,
573
- # side=side,
574
- # type=OrderType.MARKET,
575
- # amount=remaining,
576
- # position_side=position_side,
577
- # time_in_force=TimeInForce.IOC,
578
- # kwargs=kwargs,
579
- # ),
580
- # account_type=account_type,
581
- # )
582
- # if order.success:
583
- # algo_order.orders.append(order.uuid)
584
- # self._cache._order_status_update(algo_order)
585
- # else:
586
- # algo_order.status = AlgoOrderStatus.FAILED
587
- # self._cache._order_status_update(algo_order)
588
- # self._log.error(
589
- # f"TWAP ORDER FAILED: symbol: {symbol}, side: {side}"
590
- # )
591
- # break
592
- # else:
593
- # if amount_list:
594
- # amount_list[-1] += remaining
595
- # await asyncio.sleep(check_interval)
596
- # elapsed_time += check_interval
597
- # else:
598
- # price = self._cal_limit_order_price(
599
- # symbol=symbol,
600
- # side=side,
601
- # market=market,
602
- # )
603
- # amount = amount_list.pop()
604
- # if amount_list:
605
- # order_submit = CreateOrderSubmit(
606
- # symbol=symbol,
607
- # instrument_id=instrument_id,
608
- # submit_type=SubmitType.CREATE,
609
- # type=OrderType.LIMIT,
610
- # side=side,
611
- # amount=amount,
612
- # price=price,
613
- # time_in_force=TimeInForce.GTC,
614
- # position_side=position_side,
615
- # kwargs=kwargs,
616
- # )
617
- # else:
618
- # order_submit = CreateOrderSubmit(
619
- # symbol=symbol,
620
- # instrument_id=instrument_id,
621
- # submit_type=SubmitType.CREATE,
622
- # type=OrderType.MARKET,
623
- # side=side,
624
- # amount=amount,
625
- # time_in_force=TimeInForce.IOC,
626
- # position_side=position_side,
627
- # kwargs=kwargs,
628
- # )
629
- # order = await self._create_order(order_submit, account_type)
630
- # if order.success:
631
- # order_id = order.uuid
632
- # algo_order.orders.append(order_id)
633
- # self._cache._order_status_update(algo_order)
634
- # await asyncio.sleep(wait - elapsed_time)
635
- # elapsed_time = 0
636
- # else:
637
- # algo_order.status = AlgoOrderStatus.FAILED
638
- # self._cache._order_status_update(algo_order)
639
-
640
- # self._log.error(
641
- # f"TWAP ORDER FAILED: symbol: {symbol}, side: {side}, uuid: {twap_uuid}"
642
- # )
643
- # break
644
-
645
- # algo_order.status = AlgoOrderStatus.FINISHED
646
- # self._cache._order_status_update(algo_order)
647
-
648
- # self._log.info(
649
- # f"TWAP ORDER FINISHED: symbol: {symbol}, side: {side}, uuid: {twap_uuid}"
650
- # )
651
- # except asyncio.CancelledError:
652
- # algo_order.status = AlgoOrderStatus.CANCELING
653
- # self._cache._order_status_update(algo_order)
654
-
655
- # open_orders = self._cache.get_open_orders(symbol=symbol)
656
- # for uuid in open_orders.copy():
657
- # await self._cancel_order(
658
- # order_submit=CancelOrderSubmit(
659
- # symbol=symbol,
660
- # instrument_id=instrument_id,
661
- # submit_type=SubmitType.CANCEL,
662
- # uuid=uuid,
663
- # ),
664
- # account_type=account_type,
665
- # )
666
-
667
- # algo_order.status = AlgoOrderStatus.CANCELED
668
- # self._cache._order_status_update(algo_order)
669
-
670
- # self._log.info(
671
- # f"TWAP ORDER CANCELLED: symbol: {symbol}, side: {side}, uuid: {twap_uuid}"
672
- # )
673
-
674
- # async def _create_twap_order(
675
- # self, order_submit: TWAPOrderSubmit, account_type: AccountType
676
- # ):
677
- # """
678
- # Create a twap order
679
- # """
680
- # uuid = order_submit.uuid
681
- # self._task_manager.create_task(
682
- # self._twap_order(order_submit, account_type), name=uuid
683
- # )
684
-
685
- # async def _cancel_twap_order(
686
- # self, order_submit: CancelTWAPOrderSubmit, account_type: AccountType
687
- # ):
688
- # """
689
- # Cancel a twap order
690
- # """
691
- # uuid = order_submit.uuid
692
- # self._task_manager.cancel_task(uuid)
693
-
694
398
  async def _create_tp_sl_order(
695
399
  self, order_submit: TakeProfitAndStopLossOrderSubmit, account_type: AccountType
696
400
  ):
@@ -737,32 +441,47 @@ class ExecutionManagementSystem(ABC):
737
441
  }
738
442
 
739
443
  self._log.debug(f"Handling orders for account type: {account_type}")
444
+ # Each dequeued item runs as its own task so one in-flight REST round
445
+ # trip does not block the rest of the queue. The semaphore throttles
446
+ # the dequeue itself: excess items stay in the priority queue (where
447
+ # cancels can still jump ahead) instead of piling up as tasks parked
448
+ # on a FIFO semaphore. Rate-limit quota compliance stays with the
449
+ # per-exchange throttled-py limiters.
450
+ semaphore = asyncio.Semaphore(self._concurrency)
740
451
  while True:
741
452
  queue_item = await queue.get()
742
- order_submit: Any | None = None
743
- submit_type: SubmitType | None = None
744
- try:
745
- (order_submit, submit_type) = queue_item
746
- self._log.debug(f"[ORDER SUBMIT]: {order_submit}")
747
- handler = submit_handlers[submit_type]
748
- await handler(order_submit, account_type)
749
- except Exception:
750
- self._log.error(
751
- "Order submit handler failed, "
752
- f"account_type={account_type}, submit_type={submit_type}, "
753
- f"order_submit={order_submit}, queue_item={queue_item}\n"
754
- f"Traceback: {traceback.format_exc()}"
453
+ await semaphore.acquire()
454
+ (order_submit, submit_type) = queue_item
455
+ self._log.debug(f"[ORDER SUBMIT]: {order_submit}")
456
+ self._task_manager.create_task(
457
+ self._run_submit(
458
+ submit_handlers[submit_type],
459
+ order_submit,
460
+ submit_type,
461
+ account_type,
462
+ semaphore,
755
463
  )
756
- finally:
757
- try:
758
- queue.task_done()
759
- except Exception:
760
- self._log.error(
761
- "Order submit queue task_done failed, "
762
- f"account_type={account_type}, submit_type={submit_type}, "
763
- f"order_submit={order_submit}, queue_item={queue_item}\n"
764
- f"Traceback: {traceback.format_exc()}"
765
- )
464
+ )
465
+
466
+ async def _run_submit(
467
+ self,
468
+ handler: Any,
469
+ order_submit: Any,
470
+ submit_type: SubmitType,
471
+ account_type: AccountType,
472
+ semaphore: asyncio.Semaphore,
473
+ ) -> None:
474
+ try:
475
+ await handler(order_submit, account_type)
476
+ except Exception:
477
+ self._log.error(
478
+ "Order submit handler failed, "
479
+ f"account_type={account_type}, submit_type={submit_type}, "
480
+ f"order_submit={order_submit}\n"
481
+ f"Traceback: {traceback.format_exc()}"
482
+ )
483
+ finally:
484
+ semaphore.release()
766
485
 
767
486
  async def _create_batch_orders(
768
487
  self, batch_orders: List[BatchOrderSubmit], account_type: AccountType
@@ -782,6 +501,12 @@ class ExecutionManagementSystem(ABC):
782
501
  orders=orders
783
502
  )
784
503
 
504
+ def shutdown(self) -> None:
505
+ # Stop accepting new order submits; the engine calls this before the
506
+ # final cancel-all sweep so late strategy callbacks cannot slip new
507
+ # orders past it.
508
+ self._shutdown = True
509
+
785
510
  async def start(self):
786
511
  """
787
512
  Start the order submit
@@ -135,6 +135,9 @@ class WebConfig:
135
135
  class QueueConfig:
136
136
  ems_maxsize: int = 100_000
137
137
  sms_maxsize: int = 100_000
138
+ # Max concurrent in-flight order submits per account; 1 restores strictly
139
+ # serial submission.
140
+ ems_concurrency: int = 7
138
141
 
139
142
 
140
143
  @dataclass
@@ -310,12 +310,13 @@ class Engine:
310
310
  registry=self._registry,
311
311
  order_query_config=self._config.order_query_config,
312
312
  queue_maxsize=self._config.queue_config.ems_maxsize,
313
+ ems_concurrency=self._config.queue_config.ems_concurrency,
313
314
  )
314
315
 
315
316
  for exchange_id, exchange in self._exchanges.items():
316
317
  # Check if there's a custom EMS for this exchange
317
318
  if exchange_id in self._custom_ems:
318
- self._ems[exchange_id] = self._custom_ems[exchange_id](
319
+ ems = self._custom_ems[exchange_id](
319
320
  market=exchange.market,
320
321
  cache=self._cache,
321
322
  msgbus=self._msgbus,
@@ -324,6 +325,10 @@ class Engine:
324
325
  registry=self._registry,
325
326
  queue_maxsize=self._config.queue_config.ems_maxsize,
326
327
  )
328
+ # Set directly instead of via __init__: custom EMS classes
329
+ # may predate the concurrency parameter.
330
+ ems._concurrency = self._config.queue_config.ems_concurrency
331
+ self._ems[exchange_id] = ems
327
332
  self._ems[exchange_id]._build(self._private_connectors)
328
333
  continue
329
334
 
@@ -673,6 +678,12 @@ class Engine:
673
678
  # Suppress expected shutdown exceptions
674
679
  pass
675
680
 
681
+ # Refuse any further order submits: late WS callbacks can still fire
682
+ # until the connectors are torn down below, and a new order slipping in
683
+ # after the cancel-all sweep would survive it.
684
+ for ems in self._ems.values():
685
+ ems.shutdown()
686
+
676
687
  # Cancel all open orders if configured
677
688
  if self._config.exit_after_cancel:
678
689
  await self._cancel_all_open_orders()
@@ -35,6 +35,7 @@ class BuildContext:
35
35
  registry: Any # OrderRegistry
36
36
  order_query_config: OrderQueryConfig
37
37
  queue_maxsize: int = 0
38
+ ems_concurrency: int = 3
38
39
 
39
40
 
40
41
  class ExchangeFactory(ABC):