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,213 @@
1
+ import warnings
2
+ import ccxt
3
+ from abc import ABC, abstractmethod
4
+ from typing import Dict, Any, List
5
+ import nexuslog as logging
6
+
7
+ from walrasquant.schema import BaseMarket, InstrumentId
8
+ from walrasquant.constants import ExchangeType, AccountType, ConfigType
9
+
10
+
11
+ class ExchangeManager(ABC):
12
+ def __init__(self, config: ConfigType | None = None):
13
+ if config is None:
14
+ raise ValueError("config is required")
15
+
16
+ self.config: ConfigType = config
17
+ self.api_key = config.get("apiKey")
18
+ self.secret = config.get("secret")
19
+ self.exchange_id = ExchangeType(config["exchange_id"])
20
+ self.api = self._init_exchange()
21
+ self._log = logging.getLogger(name=type(self).__name__)
22
+ self.is_testnet = config.get("sandbox", False)
23
+ self.market: Dict[str, BaseMarket] = {}
24
+ self.market_id: Dict[str, str] = {}
25
+
26
+ if not self.api_key or not self.secret:
27
+ warnings.warn(
28
+ "API Key and Secret not provided, So some features related to trading will not work"
29
+ )
30
+ self.load_markets()
31
+
32
+ def _init_exchange(self) -> ccxt.Exchange:
33
+ """
34
+ Initialize the exchange
35
+ """
36
+ try:
37
+ exchange_class = getattr(ccxt, self.config["exchange_id"])
38
+ except AttributeError:
39
+ raise AttributeError(
40
+ f"Exchange {self.config['exchange_id']} is not supported"
41
+ ) from None
42
+
43
+ api = exchange_class(self.config)
44
+ api.set_sandbox_mode(
45
+ self.config.get("sandbox", False)
46
+ ) # Set sandbox mode if demo trade is enabled
47
+ return api
48
+
49
+ def _parse_symbol(self, mkt: BaseMarket, exchange_suffix: str) -> str:
50
+ """
51
+ Parse the symbol for the exchange
52
+ """
53
+ if mkt.spot:
54
+ return f"{mkt.base}{mkt.quote}.{exchange_suffix}"
55
+ elif mkt.option:
56
+ symbol = mkt.symbol
57
+ parts = symbol.split("-")
58
+ expiry = parts[1]
59
+ strike = parts[2]
60
+ option_type = parts[3]
61
+ return f"{mkt.base}{mkt.quote}-{expiry}-{strike}-{option_type}.{exchange_suffix}"
62
+ elif mkt.future:
63
+ symbol = mkt.symbol
64
+ expiry_suffix = symbol.split("-")[-1]
65
+ return f"{mkt.base}{mkt.quote}-{expiry_suffix}.{exchange_suffix}"
66
+ elif mkt.linear:
67
+ return f"{mkt.base}{mkt.quote}-PERP.{exchange_suffix}"
68
+ elif mkt.inverse:
69
+ return f"{mkt.base}{mkt.quote}-PERP.{exchange_suffix}"
70
+ raise ValueError(f"Unsupported market type for symbol {mkt.symbol}")
71
+
72
+ @abstractmethod
73
+ def load_markets(self, reload: bool = False) -> None:
74
+ pass
75
+
76
+ def linear(
77
+ self,
78
+ base: str | None = None,
79
+ quote: str | None = None,
80
+ exclude: List[str] | None = None,
81
+ ) -> List[str]:
82
+ symbols = []
83
+ for symbol, market in self.market.items():
84
+ if not (
85
+ market.linear
86
+ and market.active
87
+ and not market.future
88
+ and not market.option
89
+ ):
90
+ continue
91
+
92
+ base_match = base is None or market.base == base
93
+ quote_match = quote is None or market.quote == quote
94
+
95
+ if (
96
+ base_match
97
+ and quote_match
98
+ and (exclude is None or symbol not in exclude)
99
+ ):
100
+ symbols.append(symbol)
101
+ return symbols
102
+
103
+ def inverse(
104
+ self,
105
+ base: str | None = None,
106
+ quote: str | None = None,
107
+ exclude: List[str] | None = None,
108
+ ) -> List[str]:
109
+ symbols = []
110
+ for symbol, market in self.market.items():
111
+ if not (
112
+ market.inverse
113
+ and market.active
114
+ and not market.future
115
+ and not market.option
116
+ ):
117
+ continue
118
+
119
+ base_match = base is None or market.base == base
120
+ quote_match = quote is None or market.quote == quote
121
+
122
+ if (
123
+ base_match
124
+ and quote_match
125
+ and (exclude is None or symbol not in exclude)
126
+ ):
127
+ symbols.append(symbol)
128
+ return symbols
129
+
130
+ def spot(
131
+ self,
132
+ base: str | None = None,
133
+ quote: str | None = None,
134
+ exclude: List[str] | None = None,
135
+ ) -> List[str]:
136
+ symbols = []
137
+ for symbol, market in self.market.items():
138
+ if not (market.spot and market.active and not market.option):
139
+ continue
140
+
141
+ base_match = base is None or market.base == base
142
+ quote_match = quote is None or market.quote == quote
143
+
144
+ if (
145
+ base_match
146
+ and quote_match
147
+ and (exclude is None or symbol not in exclude)
148
+ ):
149
+ symbols.append(symbol)
150
+ return symbols
151
+
152
+ def future(
153
+ self,
154
+ base: str | None = None,
155
+ quote: str | None = None,
156
+ exclude: List[str] | None = None,
157
+ ) -> List[str]:
158
+ symbols = []
159
+ for symbol, market in self.market.items():
160
+ if not (market.future and market.active and not market.option):
161
+ continue
162
+
163
+ base_match = base is None or market.base == base
164
+ quote_match = quote is None or market.quote == quote
165
+
166
+ if (
167
+ base_match
168
+ and quote_match
169
+ and (exclude is None or symbol not in exclude)
170
+ ):
171
+ symbols.append(symbol)
172
+ return symbols
173
+
174
+ def option(
175
+ self,
176
+ base: str | None = None,
177
+ quote: str | None = None,
178
+ exclude: List[str] | None = None,
179
+ ) -> List[str]:
180
+ symbols = []
181
+ for symbol, market in self.market.items():
182
+ if not market.active:
183
+ continue
184
+
185
+ base_match = base is None or market.base == base
186
+ quote_match = quote is None or market.quote == quote
187
+
188
+ if (
189
+ base_match
190
+ and quote_match
191
+ and (exclude is None or symbol not in exclude)
192
+ ):
193
+ symbols.append(symbol)
194
+ return symbols
195
+
196
+ @abstractmethod
197
+ def validate_public_connector_config(
198
+ self, account_type: AccountType, basic_config: Any
199
+ ) -> None:
200
+ """Validate public connector configuration for this exchange"""
201
+ pass
202
+
203
+ @abstractmethod
204
+ def validate_public_connector_limits(
205
+ self, existing_connectors: Dict[AccountType, Any]
206
+ ) -> None:
207
+ """Validate public connector limits for this exchange"""
208
+ pass
209
+
210
+ @abstractmethod
211
+ def instrument_id_to_account_type(self, instrument_id: InstrumentId) -> AccountType:
212
+ """Convert an instrument ID to the appropriate account type for this exchange"""
213
+ pass
@@ -0,0 +1,428 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ import asyncio
5
+ from typing import Dict, List, Literal, Optional, TYPE_CHECKING
6
+ from decimal import Decimal
7
+ import nexuslog as logging
8
+
9
+ from decimal import ROUND_HALF_UP, ROUND_CEILING, ROUND_FLOOR
10
+ from walrasquant.constants import AccountType, ExchangeType
11
+ from walrasquant.core.cache import AsyncCache
12
+ from walrasquant.core.nautilius_core import LiveClock, MessageBus
13
+ from walrasquant.core.registry import OrderRegistry
14
+ from walrasquant.core.entity import TaskManager
15
+ from walrasquant.base.api_client import ApiClient
16
+ from walrasquant.base.ws_client import WSClient
17
+ from walrasquant.schema import (
18
+ Order,
19
+ BaseMarket,
20
+ BatchOrderSubmit,
21
+ CancelOrderSubmit,
22
+ )
23
+ from walrasquant.constants import (
24
+ OrderSide,
25
+ OrderType,
26
+ TimeInForce,
27
+ TriggerType,
28
+ OrderStatus,
29
+ )
30
+
31
+ if TYPE_CHECKING:
32
+ from walrasquant.config import OrderQueryConfig
33
+
34
+
35
+ class OrderManagementSystem(ABC):
36
+ def __init__(
37
+ self,
38
+ account_type: AccountType,
39
+ market: Dict[str, BaseMarket],
40
+ market_id: Dict[str, str],
41
+ registry: OrderRegistry,
42
+ cache: AsyncCache,
43
+ api_client: ApiClient,
44
+ ws_client: WSClient,
45
+ exchange_id: ExchangeType,
46
+ clock: LiveClock,
47
+ msgbus: MessageBus,
48
+ task_manager: TaskManager,
49
+ order_query_config: OrderQueryConfig,
50
+ ):
51
+ self._log = logging.getLogger(name=type(self).__name__)
52
+ self._market = market
53
+ self._market_id = market_id
54
+ self._registry = registry
55
+ self._account_type = account_type
56
+ self._cache = cache
57
+ self._api_client = api_client
58
+ self._ws_client = ws_client
59
+ self._exchange_id = exchange_id
60
+ self._clock = clock
61
+ self._msgbus = msgbus
62
+ self._task_manager = task_manager
63
+ self._order_query_config = order_query_config
64
+ self._querying_oids: set[str] = set()
65
+ self._last_query_ms_by_oid: dict[str, int] = {}
66
+ self._order_query_started = False
67
+
68
+ async def _async_init(self):
69
+ """Two-phase initialization: call after __init__ in async context."""
70
+ await self._init_account_balance()
71
+ await self._init_position()
72
+ await self._position_mode_check()
73
+
74
+ def order_status_update(self, order: Order):
75
+ if order.oid is None:
76
+ return
77
+
78
+ if not self._registry.is_registered(order.oid):
79
+ return
80
+
81
+ valid = self._cache._order_status_update(order) # INITIALIZED -> PENDING
82
+ match order.status:
83
+ case OrderStatus.PENDING:
84
+ self._log.debug(f"ORDER STATUS PENDING: {str(order)}")
85
+ self._msgbus.send(endpoint="pending", msg=order)
86
+ case OrderStatus.FAILED:
87
+ self._log.debug(f"ORDER STATUS FAILED: {str(order)}")
88
+ self._msgbus.send(endpoint="failed", msg=order)
89
+ case OrderStatus.ACCEPTED:
90
+ self._log.debug(f"ORDER STATUS ACCEPTED: {str(order)}")
91
+ self._msgbus.send(endpoint="accepted", msg=order)
92
+ case OrderStatus.PARTIALLY_FILLED:
93
+ self._log.debug(f"ORDER STATUS PARTIALLY FILLED: {str(order)}")
94
+ self._msgbus.send(endpoint="partially_filled", msg=order)
95
+ case OrderStatus.CANCELED:
96
+ self._log.debug(f"ORDER STATUS CANCELED: {str(order)}")
97
+ self._msgbus.send(endpoint="canceled", msg=order)
98
+ case OrderStatus.CANCELING:
99
+ self._log.debug(f"ORDER STATUS CANCELING: {str(order)}")
100
+ self._msgbus.send(endpoint="canceling", msg=order)
101
+ case OrderStatus.CANCEL_FAILED:
102
+ self._log.debug(f"ORDER STATUS CANCEL FAILED: {str(order)}")
103
+ self._msgbus.send(endpoint="cancel_failed", msg=order)
104
+ case OrderStatus.FILLED:
105
+ self._log.debug(f"ORDER STATUS FILLED: {str(order)}")
106
+ self._msgbus.send(endpoint="filled", msg=order)
107
+ case OrderStatus.EXPIRED:
108
+ self._log.debug(f"ORDER STATUS EXPIRED: {str(order)}")
109
+ self._msgbus.send(endpoint="expired", msg=order)
110
+
111
+ if valid and order.status == OrderStatus.CANCEL_FAILED:
112
+ self._schedule_query_order(order, reason="cancel_failed")
113
+
114
+ if valid and order.is_closed:
115
+ self._last_query_ms_by_oid.pop(order.oid, None)
116
+ self._registry.unregister_order(order.oid)
117
+ self._registry.unregister_tmp_order(order.oid)
118
+
119
+ def start_order_query(self) -> None:
120
+ if not self._order_query_enabled or self._order_query_started:
121
+ return
122
+ if self._order_query_config.check_interval_seconds == -1:
123
+ return
124
+ self._order_query_started = True
125
+ self._task_manager.create_task(
126
+ self._order_query_scan_loop(),
127
+ name=f"order_query:{self._exchange_id.value}:{self._account_type.value}",
128
+ )
129
+
130
+ @property
131
+ def _order_query_enabled(self) -> bool:
132
+ return self._order_query_config.enabled
133
+
134
+ async def _order_query_scan_loop(self) -> None:
135
+ try:
136
+ while True:
137
+ await asyncio.sleep(self._order_query_config.check_interval_seconds)
138
+ self._scan_timed_out_orders()
139
+ except asyncio.CancelledError:
140
+ raise
141
+ except Exception as exc:
142
+ self._log.error(f"Order query scan loop stopped unexpectedly: {exc}")
143
+ raise
144
+
145
+ def _scan_timed_out_orders(self) -> None:
146
+ now_ms = self._clock.timestamp_ms()
147
+ oids = self._cache.get_open_orders(
148
+ exchange=self._exchange_id,
149
+ include_canceling=True,
150
+ )
151
+ for oid in oids:
152
+ order = self._cache.get_order(oid).value_or(None)
153
+ if order is None:
154
+ continue
155
+ if order.oid is None or order.timestamp is None:
156
+ continue
157
+ if not self._should_query_order(order):
158
+ continue
159
+
160
+ timeout_seconds = self._order_query_config.timeout_seconds.get(order.status)
161
+ if timeout_seconds is None:
162
+ continue
163
+
164
+ baseline_ms = max(
165
+ int(order.timestamp),
166
+ self._last_query_ms_by_oid.get(order.oid, 0),
167
+ )
168
+ age_seconds = (now_ms - baseline_ms) / 1000
169
+ if age_seconds >= timeout_seconds:
170
+ self._schedule_query_order(order, reason="timeout")
171
+
172
+ def _should_query_order(self, order: Order) -> bool:
173
+ if not self._order_query_enabled:
174
+ return False
175
+ if order.exchange != self._exchange_id:
176
+ return False
177
+ market = self._market.get(order.symbol)
178
+ if market is None:
179
+ return False
180
+ return self._supports_query_market(market)
181
+
182
+ def _supports_query_market(self, market: BaseMarket) -> bool:
183
+ return True
184
+
185
+ def _schedule_query_order(self, order: Order, *, reason: str) -> None:
186
+ if not self._should_query_order(order):
187
+ return
188
+
189
+ oid = order.oid
190
+ if oid is None:
191
+ return
192
+ if oid in self._querying_oids:
193
+ return
194
+
195
+ self._querying_oids.add(oid)
196
+ self._last_query_ms_by_oid[oid] = self._clock.timestamp_ms()
197
+ self._task_manager.create_task(
198
+ self._query_order_safe(order=order, reason=reason),
199
+ name=(
200
+ f"query_order:{self._exchange_id.value}:"
201
+ f"{self._account_type.value}:{oid}"
202
+ ),
203
+ )
204
+
205
+ async def _query_order_safe(self, *, order: Order, reason: str) -> None:
206
+ oid = order.oid
207
+ if oid is None:
208
+ return
209
+
210
+ try:
211
+ self._log.debug(
212
+ f"Querying order status: reason={reason} symbol={order.symbol} oid={oid}"
213
+ )
214
+ queried_order = await self.query_order(oid=oid, symbol=order.symbol)
215
+ if queried_order is None:
216
+ self._log.warning(
217
+ f"Order status query returned no order: reason={reason} "
218
+ f"symbol={order.symbol} oid={oid}"
219
+ )
220
+ return
221
+ if queried_order.oid is None:
222
+ self._log.error(
223
+ f"Order status query returned order without oid: "
224
+ f"symbol={order.symbol} oid={oid}"
225
+ )
226
+ return
227
+ self.order_status_update(queried_order)
228
+ except asyncio.CancelledError:
229
+ raise
230
+ except Exception as exc:
231
+ self._log.error(
232
+ f"Order status query failed: reason={reason} "
233
+ f"symbol={order.symbol} oid={oid}: {exc}"
234
+ )
235
+ finally:
236
+ self._querying_oids.discard(oid)
237
+
238
+ def _rate_limit_failed_order(
239
+ self,
240
+ *,
241
+ oid: str,
242
+ symbol: str,
243
+ side: OrderSide,
244
+ type: OrderType,
245
+ amount: Decimal,
246
+ price: Decimal | None,
247
+ time_in_force: TimeInForce | None,
248
+ reduce_only: bool,
249
+ exc: Exception,
250
+ ) -> Order:
251
+ return Order(
252
+ oid=oid,
253
+ exchange=self._exchange_id,
254
+ timestamp=self._clock.timestamp_ms(),
255
+ symbol=symbol,
256
+ type=type,
257
+ side=side,
258
+ amount=amount,
259
+ price=float(price) if price else None,
260
+ time_in_force=time_in_force,
261
+ status=OrderStatus.FAILED,
262
+ filled=Decimal(0),
263
+ remaining=amount,
264
+ reduce_only=reduce_only,
265
+ reason=f"rate_limit (retry_after={getattr(exc, 'retry_after', None)}): {exc}",
266
+ )
267
+
268
+ async def wait_ready(self):
269
+ """Wait for the WebSocket client(s) to be ready.
270
+
271
+ This method waits for the main WebSocket client and optionally
272
+ for the WebSocket API client if it exists.
273
+ """
274
+ await self._ws_client.wait_ready()
275
+ # Check if subclass has a WebSocket API client
276
+ ws_api_client = getattr(self, "_ws_api_client", None)
277
+ if ws_api_client is not None:
278
+ await ws_api_client.wait_ready()
279
+
280
+ def _price_to_precision(
281
+ self,
282
+ symbol: str,
283
+ price: float,
284
+ mode: Literal["round", "ceil", "floor"] = "round",
285
+ ) -> Decimal:
286
+ """
287
+ Convert the price to the precision of the market
288
+ """
289
+ market = self._market[symbol]
290
+ price_decimal: Decimal = Decimal(str(price))
291
+
292
+ decimal = market.precision.price
293
+
294
+ if decimal >= 1:
295
+ exp = Decimal(int(decimal))
296
+ precision_decimal = Decimal("1")
297
+ else:
298
+ exp = Decimal("1")
299
+ precision_decimal = Decimal(str(decimal))
300
+
301
+ if mode == "round":
302
+ format_price = (price_decimal / exp).quantize(
303
+ precision_decimal, rounding=ROUND_HALF_UP
304
+ ) * exp
305
+ elif mode == "ceil":
306
+ format_price = (price_decimal / exp).quantize(
307
+ precision_decimal, rounding=ROUND_CEILING
308
+ ) * exp
309
+ elif mode == "floor":
310
+ format_price = (price_decimal / exp).quantize(
311
+ precision_decimal, rounding=ROUND_FLOOR
312
+ ) * exp
313
+ return format_price
314
+
315
+ @abstractmethod
316
+ async def _init_account_balance(self):
317
+ """Initialize the account balance"""
318
+ pass
319
+
320
+ @abstractmethod
321
+ async def _init_position(self):
322
+ """Initialize the position"""
323
+ pass
324
+
325
+ @abstractmethod
326
+ async def _position_mode_check(self):
327
+ """Check the position mode"""
328
+ pass
329
+
330
+ @abstractmethod
331
+ async def create_tp_sl_order(
332
+ self,
333
+ oid: str,
334
+ symbol: str,
335
+ side: OrderSide,
336
+ type: OrderType,
337
+ amount: Decimal,
338
+ price: Decimal | None = None,
339
+ time_in_force: TimeInForce | None = TimeInForce.GTC,
340
+ tp_order_type: OrderType | None = None,
341
+ tp_trigger_price: Decimal | None = None,
342
+ tp_price: Decimal | None = None,
343
+ tp_trigger_type: TriggerType | None = TriggerType.LAST_PRICE,
344
+ sl_order_type: OrderType | None = None,
345
+ sl_trigger_price: Decimal | None = None,
346
+ sl_price: Decimal | None = None,
347
+ sl_trigger_type: TriggerType | None = TriggerType.LAST_PRICE,
348
+ **kwargs,
349
+ ) -> Order:
350
+ """Create a take profit and stop loss order"""
351
+ pass
352
+
353
+ @abstractmethod
354
+ async def create_order(
355
+ self,
356
+ oid: str,
357
+ symbol: str,
358
+ side: OrderSide,
359
+ type: OrderType,
360
+ amount: Decimal,
361
+ price: Optional[Decimal],
362
+ time_in_force: Optional[TimeInForce],
363
+ reduce_only: bool,
364
+ **kwargs,
365
+ ) -> Order:
366
+ """Create an order"""
367
+ pass
368
+
369
+ @abstractmethod
370
+ async def create_order_ws(
371
+ self,
372
+ oid: str,
373
+ symbol: str,
374
+ side: OrderSide,
375
+ type: OrderType,
376
+ amount: Decimal,
377
+ price: Optional[Decimal],
378
+ time_in_force: Optional[TimeInForce],
379
+ reduce_only: bool,
380
+ **kwargs,
381
+ ):
382
+ pass
383
+
384
+ @abstractmethod
385
+ async def create_batch_orders(
386
+ self,
387
+ orders: List[BatchOrderSubmit],
388
+ ) -> List[Order]:
389
+ """Create a batch of orders"""
390
+ pass
391
+
392
+ @abstractmethod
393
+ async def cancel_order(self, oid: str, symbol: str, **kwargs) -> Order:
394
+ """Cancel an order"""
395
+ pass
396
+
397
+ @abstractmethod
398
+ async def cancel_order_ws(self, oid: str, symbol: str, **kwargs):
399
+ """Cancel an order"""
400
+ pass
401
+
402
+ @abstractmethod
403
+ async def query_order(self, oid: str, symbol: str) -> Order | None:
404
+ """Query an order by the local/client order id."""
405
+ pass
406
+
407
+ @abstractmethod
408
+ async def modify_order(
409
+ self,
410
+ oid: str,
411
+ symbol: str,
412
+ side: OrderSide | None = None,
413
+ price: Decimal | None = None,
414
+ amount: Decimal | None = None,
415
+ **kwargs,
416
+ ) -> Order:
417
+ """Modify an order"""
418
+ pass
419
+
420
+ @abstractmethod
421
+ async def cancel_batch_orders(self, orders: List[CancelOrderSubmit]) -> None:
422
+ """Cancel multiple orders in a single batch request"""
423
+ pass
424
+
425
+ @abstractmethod
426
+ async def cancel_all_orders(self, symbol: str) -> bool:
427
+ """Cancel all orders"""
428
+ pass