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
walrasquant/base/ems.py
ADDED
|
@@ -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
|
+
)
|