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,794 @@
1
+ import asyncio
2
+ import traceback
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, Mapping, List, Dict, cast
5
+ from typing import Literal
6
+ from decimal import Decimal
7
+ import nexuslog as logging
8
+
9
+ from decimal import ROUND_HALF_UP, ROUND_CEILING, ROUND_FLOOR
10
+
11
+ from walrasquant.schema import BaseMarket
12
+ from walrasquant.core.entity import TaskManager
13
+ from walrasquant.core.nautilius_core import MessageBus, LiveClock
14
+ from walrasquant.core.cache import AsyncCache
15
+ from walrasquant.core.registry import OrderRegistry
16
+ from walrasquant.constants import (
17
+ AccountType,
18
+ SubmitType,
19
+ # OrderSide,
20
+ # AlgoOrderStatus,
21
+ # TimeInForce,
22
+ )
23
+ from walrasquant.schema import (
24
+ InstrumentId,
25
+ OrderSubmit,
26
+ # AlgoOrder,
27
+ TakeProfitAndStopLossOrderSubmit,
28
+ CreateOrderSubmit,
29
+ CancelOrderSubmit,
30
+ CancelAllOrderSubmit,
31
+ ModifyOrderSubmit,
32
+ # TWAPOrderSubmit,
33
+ # CancelTWAPOrderSubmit,
34
+ BatchOrderSubmit,
35
+ )
36
+ from walrasquant.base.connector import PrivateConnector
37
+
38
+
39
+ _CREATE_SUBMIT_TYPES: frozenset = frozenset({SubmitType.CREATE, SubmitType.CREATE_WS})
40
+
41
+
42
+ def _item_priority(item: tuple) -> int:
43
+ """Return the scheduling priority for a queue item.
44
+
45
+ Priority scale (lower = higher priority):
46
+ 0 cancel
47
+ 1 modify
48
+ 2 reduce-only taker (close position, immediate)
49
+ 3 reduce-only maker (close position, passive)
50
+ 4 taker open (open position, immediate)
51
+ 5 maker open (open position, passive)
52
+ """
53
+ order_submit, submit_type = item
54
+ if submit_type not in _CREATE_SUBMIT_TYPES:
55
+ return submit_type.priority
56
+ if order_submit.reduce_only and order_submit.type.is_market:
57
+ return 2
58
+ if order_submit.reduce_only:
59
+ return 3
60
+ if order_submit.type.is_market:
61
+ return 4
62
+ return 5
63
+
64
+
65
+ class PriorityOrderQueue:
66
+ """Wrapper around asyncio.PriorityQueue that exposes the same interface as asyncio.Queue."""
67
+
68
+ def __init__(self, maxsize: int = 0):
69
+ self._queue: asyncio.PriorityQueue = asyncio.PriorityQueue(maxsize=maxsize)
70
+ self._seq: int = 0
71
+
72
+ @property
73
+ def maxsize(self) -> int:
74
+ return self._queue.maxsize
75
+
76
+ def qsize(self) -> int:
77
+ return self._queue.qsize()
78
+
79
+ def put_nowait(self, item: tuple, priority: int) -> None:
80
+ self._queue.put_nowait((priority, self._seq, item))
81
+ self._seq += 1
82
+
83
+ async def put(self, item: tuple, priority: int) -> None:
84
+ seq = self._seq
85
+ self._seq += 1
86
+ await self._queue.put((priority, seq, item))
87
+
88
+ async def get(self) -> tuple:
89
+ _, _, item = await self._queue.get()
90
+ return item
91
+
92
+ def task_done(self) -> None:
93
+ self._queue.task_done()
94
+
95
+
96
+ class ExecutionManagementSystem(ABC):
97
+ _BATCH_CHUNK_SIZE: int = 20
98
+
99
+ def __init__(
100
+ self,
101
+ market: Mapping[str, BaseMarket],
102
+ cache: AsyncCache,
103
+ msgbus: MessageBus,
104
+ clock: LiveClock,
105
+ task_manager: TaskManager,
106
+ registry: OrderRegistry,
107
+ queue_maxsize: int = 100_000,
108
+ ):
109
+ self._log = logging.getLogger(name=type(self).__name__)
110
+
111
+ self._market = market
112
+ self._cache = cache
113
+ self._msgbus = msgbus
114
+ self._task_manager = task_manager
115
+ self._registry = registry
116
+ self._clock = clock
117
+ self._queue_maxsize = queue_maxsize
118
+ self._order_submit_queues: Dict[AccountType, PriorityOrderQueue] = {}
119
+ self._private_connectors: Dict[AccountType, PrivateConnector] = {}
120
+ self._pending_create_oids: set[str] = set()
121
+
122
+ def _safe_put(self, queue: PriorityOrderQueue, item: Any) -> None:
123
+ # Order submits may originate off the event loop (e.g. threaded scheduled
124
+ # jobs, web webhooks). asyncio.Queue is not thread-safe, and touching it
125
+ # off-loop silently corrupts the loop state instead of raising. So decide
126
+ # the thread BEFORE touching the queue: on the loop, put inline; otherwise
127
+ # marshal the whole put onto the loop with call_soon_threadsafe.
128
+ try:
129
+ asyncio.get_running_loop() # raises only if this thread has no loop
130
+ on_loop = True
131
+ except RuntimeError:
132
+ on_loop = False
133
+
134
+ if on_loop:
135
+ self._do_put(queue, item)
136
+ else:
137
+ self._task_manager.loop.call_soon_threadsafe(self._do_put, queue, item)
138
+
139
+ def _do_put(self, queue: PriorityOrderQueue, item: Any) -> None:
140
+ # Always runs on the loop thread: queue access + bookkeeping are serialized.
141
+ order_submit, submit_type = item
142
+ priority = _item_priority(item)
143
+
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
+ if queue.maxsize == 0 or queue.qsize() < queue.maxsize:
151
+ try:
152
+ queue.put_nowait(item, priority=priority)
153
+ return
154
+ except asyncio.QueueFull:
155
+ self._log.warning(
156
+ "Order submit queue is full, falling back to async put"
157
+ )
158
+ pass
159
+ task = asyncio.get_running_loop().create_task(
160
+ queue.put(item, priority=priority)
161
+ )
162
+ task.add_done_callback(self._on_queue_put_done)
163
+
164
+ def _on_queue_put_done(self, task: asyncio.Task) -> None:
165
+ if task.cancelled():
166
+ return
167
+ exc = task.exception()
168
+ if exc:
169
+ self._log.error(f"Queue put failed: {exc!r}")
170
+
171
+ def _build(self, private_connectors: Dict[AccountType, PrivateConnector]):
172
+ self._private_connectors = private_connectors
173
+ self._build_order_submit_queues()
174
+ self._set_account_type()
175
+
176
+ def _amount_to_precision(
177
+ self,
178
+ symbol: str,
179
+ amount: float,
180
+ mode: Literal["round", "ceil", "floor"] = "round",
181
+ ) -> Decimal:
182
+ """
183
+ Convert the amount to the precision of the market
184
+ """
185
+ market = self._market[symbol]
186
+ amount_decimal: Decimal = Decimal(str(amount))
187
+ precision = market.precision.amount
188
+
189
+ if precision >= 1:
190
+ exp = Decimal(int(precision))
191
+ precision_decimal = Decimal("1")
192
+ else:
193
+ exp = Decimal("1")
194
+ precision_decimal = Decimal(str(precision))
195
+
196
+ if mode == "round":
197
+ format_amount = (amount_decimal / exp).quantize(
198
+ precision_decimal, rounding=ROUND_HALF_UP
199
+ ) * exp
200
+ elif mode == "ceil":
201
+ format_amount = (amount_decimal / exp).quantize(
202
+ precision_decimal, rounding=ROUND_CEILING
203
+ ) * exp
204
+ elif mode == "floor":
205
+ format_amount = (amount_decimal / exp).quantize(
206
+ precision_decimal, rounding=ROUND_FLOOR
207
+ ) * exp
208
+ return format_amount
209
+
210
+ def _price_to_precision(
211
+ self,
212
+ symbol: str,
213
+ price: float,
214
+ mode: Literal["round", "ceil", "floor"] = "round",
215
+ ) -> Decimal:
216
+ """
217
+ Convert the price to the precision of the market
218
+ """
219
+ market = self._market[symbol]
220
+ price_decimal: Decimal = Decimal(str(price))
221
+
222
+ decimal = market.precision.price
223
+
224
+ if decimal >= 1:
225
+ exp = Decimal(int(decimal))
226
+ precision_decimal = Decimal("1")
227
+ else:
228
+ exp = Decimal("1")
229
+ precision_decimal = Decimal(str(decimal))
230
+
231
+ if mode == "round":
232
+ format_price = (price_decimal / exp).quantize(
233
+ precision_decimal, rounding=ROUND_HALF_UP
234
+ ) * exp
235
+ elif mode == "ceil":
236
+ format_price = (price_decimal / exp).quantize(
237
+ precision_decimal, rounding=ROUND_CEILING
238
+ ) * exp
239
+ elif mode == "floor":
240
+ format_price = (price_decimal / exp).quantize(
241
+ precision_decimal, rounding=ROUND_FLOOR
242
+ ) * exp
243
+ return format_price
244
+
245
+ @abstractmethod
246
+ def _instrument_id_to_account_type(
247
+ self, instrument_id: InstrumentId
248
+ ) -> AccountType:
249
+ pass
250
+
251
+ @abstractmethod
252
+ def _build_order_submit_queues(self):
253
+ """
254
+ Build the order submit queues
255
+ """
256
+ pass
257
+
258
+ @abstractmethod
259
+ def _set_account_type(self):
260
+ """
261
+ Set the account type
262
+ """
263
+ pass
264
+
265
+ def _submit_order(
266
+ self,
267
+ order: (
268
+ OrderSubmit
269
+ | List[OrderSubmit]
270
+ | List[BatchOrderSubmit]
271
+ | List[CancelOrderSubmit]
272
+ ),
273
+ submit_type: SubmitType,
274
+ account_type: AccountType | None = None,
275
+ ) -> None:
276
+ if isinstance(order, list):
277
+ if not account_type:
278
+ first_order = cast(OrderSubmit, order[0])
279
+ account_type = self._instrument_id_to_account_type(
280
+ first_order.instrument_id
281
+ )
282
+ for i in range(0, len(order), self._BATCH_CHUNK_SIZE):
283
+ batch = order[i : i + self._BATCH_CHUNK_SIZE]
284
+ self._safe_put(
285
+ self._order_submit_queues[account_type], (batch, submit_type)
286
+ )
287
+ else:
288
+ if not account_type:
289
+ account_type = self._instrument_id_to_account_type(order.instrument_id)
290
+ self._safe_put(
291
+ self._order_submit_queues[account_type], (order, submit_type)
292
+ )
293
+
294
+ async def _modify_order(
295
+ self, order_submit: ModifyOrderSubmit, account_type: AccountType
296
+ ):
297
+ """
298
+ Modify an order
299
+ """
300
+ await self._private_connectors[account_type]._oms.modify_order(
301
+ oid=order_submit.oid,
302
+ symbol=order_submit.symbol,
303
+ price=order_submit.price,
304
+ amount=order_submit.amount,
305
+ side=order_submit.side,
306
+ **order_submit.kwargs,
307
+ )
308
+
309
+ async def _cancel_all_orders(
310
+ self, order_submit: CancelAllOrderSubmit, account_type: AccountType
311
+ ):
312
+ """
313
+ Cancel all orders
314
+ """
315
+ symbol = order_submit.symbol
316
+ # self._cache.mark_all_cancel_intent(symbol)
317
+ await self._private_connectors[account_type]._oms.cancel_all_orders(symbol)
318
+
319
+ async def _cancel_order(
320
+ self, order_submit: CancelOrderSubmit, account_type: AccountType
321
+ ):
322
+ """
323
+ Cancel an order
324
+ """
325
+ # self._cache.mark_cancel_intent(order_submit.oid)
326
+ self._pending_create_oids.discard(order_submit.oid)
327
+ await self._private_connectors[account_type]._oms.cancel_order(
328
+ oid=order_submit.oid,
329
+ symbol=order_submit.symbol,
330
+ **order_submit.kwargs,
331
+ )
332
+
333
+ async def _cancel_order_ws(
334
+ self, order_submit: CancelOrderSubmit, account_type: AccountType
335
+ ):
336
+ """
337
+ Cancel an order
338
+ """
339
+ # self._cache.mark_cancel_intent(order_submit.oid)
340
+ self._pending_create_oids.discard(order_submit.oid)
341
+ await self._private_connectors[account_type]._oms.cancel_order_ws(
342
+ oid=order_submit.oid,
343
+ symbol=order_submit.symbol,
344
+ **order_submit.kwargs,
345
+ )
346
+
347
+ async def _create_order(
348
+ self, order_submit: CreateOrderSubmit, account_type: AccountType
349
+ ):
350
+ """
351
+ Create an order
352
+ """
353
+ self._pending_create_oids.discard(order_submit.oid)
354
+ self._registry.register_order(order_submit.oid)
355
+ self._cache.add_inflight_order(order_submit.symbol, order_submit.oid)
356
+ await self._private_connectors[account_type]._oms.create_order(
357
+ oid=order_submit.oid,
358
+ symbol=order_submit.symbol,
359
+ side=order_submit.side,
360
+ type=order_submit.type,
361
+ amount=order_submit.amount,
362
+ price=order_submit.price,
363
+ time_in_force=order_submit.time_in_force,
364
+ reduce_only=order_submit.reduce_only,
365
+ **order_submit.kwargs,
366
+ )
367
+
368
+ async def _create_order_ws(
369
+ self, order_submit: CreateOrderSubmit, account_type: AccountType
370
+ ):
371
+ """
372
+ Create an order
373
+ """
374
+ self._pending_create_oids.discard(order_submit.oid)
375
+ self._registry.register_order(order_submit.oid)
376
+ self._cache.add_inflight_order(order_submit.symbol, order_submit.oid)
377
+ await self._private_connectors[account_type]._oms.create_order_ws(
378
+ oid=order_submit.oid,
379
+ symbol=order_submit.symbol,
380
+ side=order_submit.side,
381
+ type=order_submit.type,
382
+ amount=order_submit.amount,
383
+ price=order_submit.price,
384
+ time_in_force=order_submit.time_in_force,
385
+ reduce_only=order_submit.reduce_only,
386
+ **order_submit.kwargs,
387
+ )
388
+
389
+ @abstractmethod
390
+ def _get_min_order_amount(
391
+ self, symbol: str, market: BaseMarket, px: float
392
+ ) -> Decimal:
393
+ """
394
+ Get the minimum order amount
395
+ """
396
+ pass
397
+
398
+ @abstractmethod
399
+ def _get_max_order_amount(
400
+ self, symbol: str, market: BaseMarket, is_market: bool, px: float
401
+ ) -> Decimal:
402
+ """
403
+ Get the maximum order amount
404
+ """
405
+ pass
406
+
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
+ async def _create_tp_sl_order(
695
+ self, order_submit: TakeProfitAndStopLossOrderSubmit, account_type: AccountType
696
+ ):
697
+ self._registry.register_order(order_submit.oid)
698
+ await self._private_connectors[account_type]._oms.create_tp_sl_order(
699
+ oid=order_submit.oid,
700
+ symbol=order_submit.symbol,
701
+ side=order_submit.side,
702
+ type=order_submit.type,
703
+ amount=order_submit.amount,
704
+ price=order_submit.price,
705
+ time_in_force=order_submit.time_in_force,
706
+ tp_order_type=order_submit.tp_order_type,
707
+ tp_trigger_price=order_submit.tp_trigger_price,
708
+ tp_price=order_submit.tp_price,
709
+ tp_trigger_type=order_submit.tp_trigger_type,
710
+ sl_order_type=order_submit.sl_order_type,
711
+ sl_trigger_price=order_submit.sl_trigger_price,
712
+ sl_price=order_submit.sl_price,
713
+ sl_trigger_type=order_submit.sl_trigger_type,
714
+ **order_submit.kwargs,
715
+ )
716
+
717
+ async def _handle_submit_order(
718
+ self,
719
+ account_type: AccountType,
720
+ queue: PriorityOrderQueue,
721
+ ):
722
+ """
723
+ Handle the order submit
724
+ """
725
+ submit_handlers = {
726
+ SubmitType.CANCEL: self._cancel_order,
727
+ SubmitType.CREATE: self._create_order,
728
+ # SubmitType.TWAP: self._create_twap_order,
729
+ # SubmitType.CANCEL_TWAP: self._cancel_twap_order,
730
+ SubmitType.MODIFY: self._modify_order,
731
+ SubmitType.CANCEL_ALL: self._cancel_all_orders,
732
+ SubmitType.BATCH: self._create_batch_orders,
733
+ SubmitType.TAKE_PROFIT_AND_STOP_LOSS: self._create_tp_sl_order,
734
+ SubmitType.CREATE_WS: self._create_order_ws,
735
+ SubmitType.CANCEL_WS: self._cancel_order_ws,
736
+ SubmitType.CANCEL_BATCH: self._cancel_batch_orders,
737
+ }
738
+
739
+ self._log.debug(f"Handling orders for account type: {account_type}")
740
+ while True:
741
+ 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()}"
755
+ )
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
+ )
766
+
767
+ async def _create_batch_orders(
768
+ self, batch_orders: List[BatchOrderSubmit], account_type: AccountType
769
+ ):
770
+ for order in batch_orders:
771
+ self._registry.register_order(order.oid)
772
+ self._cache.add_inflight_order(order.symbol, order.oid)
773
+
774
+ await self._private_connectors[account_type]._oms.create_batch_orders(
775
+ orders=batch_orders,
776
+ )
777
+
778
+ async def _cancel_batch_orders(
779
+ self, orders: List[CancelOrderSubmit], account_type: AccountType
780
+ ):
781
+ await self._private_connectors[account_type]._oms.cancel_batch_orders(
782
+ orders=orders
783
+ )
784
+
785
+ async def start(self):
786
+ """
787
+ Start the order submit
788
+ """
789
+ for account_type in self._order_submit_queues.keys():
790
+ self._task_manager.create_task(
791
+ self._handle_submit_order(
792
+ account_type, self._order_submit_queues[account_type]
793
+ )
794
+ )