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
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
from enum import Enum, unique
|
|
2
|
+
from throttled.asyncio import Throttled, rate_limiter, RateLimiterType
|
|
3
|
+
from walrasquant.constants import (
|
|
4
|
+
AccountType,
|
|
5
|
+
OrderStatus,
|
|
6
|
+
PositionSide,
|
|
7
|
+
OrderSide,
|
|
8
|
+
TimeInForce,
|
|
9
|
+
OrderType,
|
|
10
|
+
KlineInterval,
|
|
11
|
+
RateLimiter,
|
|
12
|
+
TriggerType,
|
|
13
|
+
)
|
|
14
|
+
from walrasquant.error import KlineSupportedError
|
|
15
|
+
from walrasquant.exchange.okx.error import OkxRateLimitError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OkxWsApiOp(Enum):
|
|
19
|
+
PLACE_ORDER = "order"
|
|
20
|
+
BATCH_ORDERS = "batch-orders"
|
|
21
|
+
CANCEL_ORDER = "cancel-order"
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def is_place_order(self):
|
|
25
|
+
return self == self.PLACE_ORDER
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def is_batch_orders(self):
|
|
29
|
+
return self == self.BATCH_ORDERS
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def is_cancel_order(self):
|
|
33
|
+
return self == self.CANCEL_ORDER
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class OkxTriggerType(Enum):
|
|
37
|
+
NONE = ""
|
|
38
|
+
LAST_PRICE = "last"
|
|
39
|
+
INDEX_PRICE = "index"
|
|
40
|
+
MARK_PRICE = "mark"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class OkxAcctLv(Enum):
|
|
44
|
+
SPOT = "1"
|
|
45
|
+
FUTURES = "2"
|
|
46
|
+
MULTI_CURRENCY_MARGIN = "3"
|
|
47
|
+
PORTFOLIO_MARGIN = "4"
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def is_spot(self):
|
|
51
|
+
return self == self.SPOT
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def is_futures(self):
|
|
55
|
+
return self == self.FUTURES
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def is_multi_currency_margin(self):
|
|
59
|
+
return self == self.MULTI_CURRENCY_MARGIN
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def is_portfolio_margin(self):
|
|
63
|
+
return self == self.PORTFOLIO_MARGIN
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class OkxPositionMode(Enum):
|
|
67
|
+
ONE_WAY_MODE = "net_mode"
|
|
68
|
+
LONG_SHORT_MODE = "long_short_mode"
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def is_one_way_mode(self):
|
|
72
|
+
return self == self.ONE_WAY_MODE
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def is_long_short_mode(self):
|
|
76
|
+
return self == self.LONG_SHORT_MODE
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class OkxSavingsPurchaseRedemptSide(Enum):
|
|
80
|
+
PURCHASE = "purchase"
|
|
81
|
+
REDEMPT = "redempt"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class OkxKlineInterval(Enum):
|
|
85
|
+
SECOND_1 = "candle1s"
|
|
86
|
+
MINUTE_1 = "candle1m"
|
|
87
|
+
MINUTE_3 = "candle3m"
|
|
88
|
+
MINUTE_5 = "candle5m"
|
|
89
|
+
MINUTE_15 = "candle15m"
|
|
90
|
+
MINUTE_30 = "candle30m"
|
|
91
|
+
HOUR_1 = "candle1H"
|
|
92
|
+
HOUR_4 = "candle4H"
|
|
93
|
+
HOUR_6 = "candle6Hutc"
|
|
94
|
+
HOUR_12 = "candle12Hutc"
|
|
95
|
+
DAY_1 = "candle1Dutc"
|
|
96
|
+
DAY_3 = "candle3Dutc"
|
|
97
|
+
WEEK_1 = "candle1Wutc"
|
|
98
|
+
MONTH_1 = "candle1Mutc"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class OkxInstrumentType(Enum):
|
|
102
|
+
SPOT = "SPOT"
|
|
103
|
+
MARGIN = "MARGIN"
|
|
104
|
+
SWAP = "SWAP"
|
|
105
|
+
FUTURES = "FUTURES"
|
|
106
|
+
OPTION = "OPTION"
|
|
107
|
+
ANY = "ANY"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class OkxInstrumentFamily(Enum):
|
|
111
|
+
FUTURES = "FUTURES"
|
|
112
|
+
SWAP = "SWAP"
|
|
113
|
+
OPTION = "OPTION"
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class OkxAccountType(AccountType):
|
|
117
|
+
LIVE = "live"
|
|
118
|
+
# AWS = "aws" # deprecated
|
|
119
|
+
DEMO = "demo"
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def exchange_id(self):
|
|
123
|
+
return "okx"
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def is_testnet(self):
|
|
127
|
+
return self == OkxAccountType.DEMO
|
|
128
|
+
|
|
129
|
+
@property
|
|
130
|
+
def stream_url(self):
|
|
131
|
+
return STREAM_URLS[self]
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
STREAM_URLS = {
|
|
135
|
+
OkxAccountType.LIVE: "wss://ws.okx.com:8443/ws",
|
|
136
|
+
OkxAccountType.DEMO: "wss://wspap.okx.com:8443/ws",
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
REST_URLS = {
|
|
140
|
+
OkxAccountType.LIVE: "https://www.okx.com",
|
|
141
|
+
OkxAccountType.DEMO: "https://www.okx.com",
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@unique
|
|
146
|
+
class OkxTdMode(Enum):
|
|
147
|
+
CASH = "cash" # 现货
|
|
148
|
+
CROSS = "cross" # 全仓
|
|
149
|
+
ISOLATED = "isolated" # 逐仓
|
|
150
|
+
SPOT_ISOLATED = "spot_isolated" # 现货逐仓
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def is_cash(self):
|
|
154
|
+
return self == self.CASH
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def is_cross(self):
|
|
158
|
+
return self == self.CROSS
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def is_isolated(self):
|
|
162
|
+
return self == self.ISOLATED
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def is_spot_isolated(self):
|
|
166
|
+
return self == self.SPOT_ISOLATED
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@unique
|
|
170
|
+
class OkxPositionSide(Enum):
|
|
171
|
+
LONG = "long"
|
|
172
|
+
SHORT = "short"
|
|
173
|
+
NET = "net"
|
|
174
|
+
NONE = ""
|
|
175
|
+
|
|
176
|
+
def parse_to_position_side(self) -> PositionSide:
|
|
177
|
+
if self == self.NET:
|
|
178
|
+
return PositionSide.FLAT
|
|
179
|
+
elif self == self.LONG:
|
|
180
|
+
return PositionSide.LONG
|
|
181
|
+
elif self == self.SHORT:
|
|
182
|
+
return PositionSide.SHORT
|
|
183
|
+
raise RuntimeError(f"Invalid position side: {self}")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@unique
|
|
187
|
+
class OkxOrderSide(Enum):
|
|
188
|
+
BUY = "buy"
|
|
189
|
+
SELL = "sell"
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def is_buy(self):
|
|
193
|
+
return self == self.BUY
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def is_sell(self):
|
|
197
|
+
return self == self.SELL
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class OkxTimeInForce(Enum):
|
|
201
|
+
IOC = "ioc"
|
|
202
|
+
GTC = "gtc"
|
|
203
|
+
FOK = "fok"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@unique
|
|
207
|
+
class OkxOrderType(Enum):
|
|
208
|
+
MARKET = "market"
|
|
209
|
+
LIMIT = "limit"
|
|
210
|
+
POST_ONLY = "post_only" # limit only, requires "px" to be provided
|
|
211
|
+
FOK = "fok" # market order if "px" is not provided, otherwise limit order
|
|
212
|
+
IOC = "ioc" # market order if "px" is not provided, otherwise limit order
|
|
213
|
+
OPTIMAL_LIMIT_IOC = (
|
|
214
|
+
"optimal_limit_ioc" # Market order with immediate-or-cancel order
|
|
215
|
+
)
|
|
216
|
+
MMP = "mmp" # Market Maker Protection (only applicable to Option in Portfolio Margin mode)
|
|
217
|
+
MMP_AND_POST_ONLY = "mmp_and_post_only" # Market Maker Protection and Post-only order(only applicable to Option in Portfolio Margin mode)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@unique
|
|
221
|
+
class OkxOrderStatus(Enum): # "state"
|
|
222
|
+
CANCELED = "canceled"
|
|
223
|
+
LIVE = "live"
|
|
224
|
+
PARTIALLY_FILLED = "partially_filled"
|
|
225
|
+
FILLED = "filled"
|
|
226
|
+
MMP_CANCELED = "mmp_canceled"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class OkxEnumParser:
|
|
230
|
+
_okx_trigger_type_map = {
|
|
231
|
+
OkxTriggerType.LAST_PRICE: TriggerType.LAST_PRICE,
|
|
232
|
+
OkxTriggerType.INDEX_PRICE: TriggerType.INDEX_PRICE,
|
|
233
|
+
OkxTriggerType.MARK_PRICE: TriggerType.MARK_PRICE,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
_okx_kline_interval_map = {
|
|
237
|
+
OkxKlineInterval.SECOND_1: KlineInterval.SECOND_1,
|
|
238
|
+
OkxKlineInterval.MINUTE_1: KlineInterval.MINUTE_1,
|
|
239
|
+
OkxKlineInterval.MINUTE_3: KlineInterval.MINUTE_3,
|
|
240
|
+
OkxKlineInterval.MINUTE_5: KlineInterval.MINUTE_5,
|
|
241
|
+
OkxKlineInterval.MINUTE_15: KlineInterval.MINUTE_15,
|
|
242
|
+
OkxKlineInterval.MINUTE_30: KlineInterval.MINUTE_30,
|
|
243
|
+
OkxKlineInterval.HOUR_1: KlineInterval.HOUR_1,
|
|
244
|
+
OkxKlineInterval.HOUR_4: KlineInterval.HOUR_4,
|
|
245
|
+
OkxKlineInterval.HOUR_6: KlineInterval.HOUR_6,
|
|
246
|
+
OkxKlineInterval.HOUR_12: KlineInterval.HOUR_12,
|
|
247
|
+
OkxKlineInterval.DAY_1: KlineInterval.DAY_1,
|
|
248
|
+
OkxKlineInterval.DAY_3: KlineInterval.DAY_3,
|
|
249
|
+
OkxKlineInterval.WEEK_1: KlineInterval.WEEK_1,
|
|
250
|
+
OkxKlineInterval.MONTH_1: KlineInterval.MONTH_1,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
_okx_order_status_map = {
|
|
254
|
+
OkxOrderStatus.LIVE: OrderStatus.ACCEPTED,
|
|
255
|
+
OkxOrderStatus.PARTIALLY_FILLED: OrderStatus.PARTIALLY_FILLED,
|
|
256
|
+
OkxOrderStatus.FILLED: OrderStatus.FILLED,
|
|
257
|
+
OkxOrderStatus.CANCELED: OrderStatus.CANCELED,
|
|
258
|
+
OkxOrderStatus.MMP_CANCELED: OrderStatus.CANCELED,
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
_okx_position_side_map = {
|
|
262
|
+
OkxPositionSide.NET: PositionSide.FLAT,
|
|
263
|
+
OkxPositionSide.LONG: PositionSide.LONG,
|
|
264
|
+
OkxPositionSide.SHORT: PositionSide.SHORT,
|
|
265
|
+
OkxPositionSide.NONE: None,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
_okx_order_side_map = {
|
|
269
|
+
OkxOrderSide.BUY: OrderSide.BUY,
|
|
270
|
+
OkxOrderSide.SELL: OrderSide.SELL,
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
# Add reverse mapping dictionaries
|
|
274
|
+
_order_status_to_okx_map = {v: k for k, v in _okx_order_status_map.items()}
|
|
275
|
+
_position_side_to_okx_map = {
|
|
276
|
+
PositionSide.FLAT: OkxPositionSide.NET,
|
|
277
|
+
PositionSide.LONG: OkxPositionSide.LONG,
|
|
278
|
+
PositionSide.SHORT: OkxPositionSide.SHORT,
|
|
279
|
+
}
|
|
280
|
+
_order_side_to_okx_map = {v: k for k, v in _okx_order_side_map.items()}
|
|
281
|
+
|
|
282
|
+
_kline_interval_to_okx_map = {v: k for k, v in _okx_kline_interval_map.items()}
|
|
283
|
+
_trigger_type_to_okx_map = {v: k for k, v in _okx_trigger_type_map.items()}
|
|
284
|
+
|
|
285
|
+
@classmethod
|
|
286
|
+
def parse_trigger_type(cls, trigger_type: OkxTriggerType) -> TriggerType:
|
|
287
|
+
return cls._okx_trigger_type_map[trigger_type]
|
|
288
|
+
|
|
289
|
+
@classmethod
|
|
290
|
+
def parse_kline_interval(cls, interval: OkxKlineInterval) -> KlineInterval:
|
|
291
|
+
return cls._okx_kline_interval_map[interval]
|
|
292
|
+
|
|
293
|
+
# Add reverse parsing methods
|
|
294
|
+
@classmethod
|
|
295
|
+
def parse_order_status(cls, status: OkxOrderStatus) -> OrderStatus:
|
|
296
|
+
return cls._okx_order_status_map[status]
|
|
297
|
+
|
|
298
|
+
@classmethod
|
|
299
|
+
def parse_position_side(cls, side: OkxPositionSide) -> PositionSide:
|
|
300
|
+
return cls._okx_position_side_map[side] or PositionSide.FLAT
|
|
301
|
+
|
|
302
|
+
@classmethod
|
|
303
|
+
def parse_order_side(cls, side: OkxOrderSide) -> OrderSide:
|
|
304
|
+
return cls._okx_order_side_map[side]
|
|
305
|
+
|
|
306
|
+
@classmethod
|
|
307
|
+
def parse_order_type(cls, ordType: OkxOrderType) -> OrderType:
|
|
308
|
+
# TODO add parameters in future to enable parsing of all other nautilus OrderType's
|
|
309
|
+
match ordType:
|
|
310
|
+
case OkxOrderType.MARKET:
|
|
311
|
+
return OrderType.MARKET
|
|
312
|
+
case OkxOrderType.LIMIT:
|
|
313
|
+
return OrderType.LIMIT
|
|
314
|
+
case OkxOrderType.IOC:
|
|
315
|
+
return OrderType.LIMIT
|
|
316
|
+
case OkxOrderType.FOK:
|
|
317
|
+
return OrderType.LIMIT
|
|
318
|
+
case OkxOrderType.POST_ONLY:
|
|
319
|
+
return OrderType.POST_ONLY
|
|
320
|
+
case _:
|
|
321
|
+
raise NotImplementedError(
|
|
322
|
+
f"Cannot parse OrderType from OKX order type {ordType}"
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
@classmethod
|
|
326
|
+
def parse_time_in_force(cls, ordType: OkxOrderType) -> TimeInForce:
|
|
327
|
+
match ordType:
|
|
328
|
+
case OkxOrderType.MARKET:
|
|
329
|
+
return TimeInForce.GTC
|
|
330
|
+
case OkxOrderType.LIMIT:
|
|
331
|
+
return TimeInForce.GTC
|
|
332
|
+
case OkxOrderType.POST_ONLY:
|
|
333
|
+
return TimeInForce.GTC
|
|
334
|
+
case OkxOrderType.FOK:
|
|
335
|
+
return TimeInForce.FOK
|
|
336
|
+
case OkxOrderType.IOC:
|
|
337
|
+
return TimeInForce.IOC
|
|
338
|
+
case _:
|
|
339
|
+
raise NotImplementedError(
|
|
340
|
+
f"Cannot parse TimeInForce from OKX order type {ordType}"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
@classmethod
|
|
344
|
+
def to_okx_order_status(cls, status: OrderStatus) -> OkxOrderStatus:
|
|
345
|
+
return cls._order_status_to_okx_map[status]
|
|
346
|
+
|
|
347
|
+
@classmethod
|
|
348
|
+
def to_okx_position_side(cls, side: PositionSide) -> OkxPositionSide:
|
|
349
|
+
return cls._position_side_to_okx_map[side]
|
|
350
|
+
|
|
351
|
+
@classmethod
|
|
352
|
+
def to_okx_order_side(cls, side: OrderSide) -> OkxOrderSide:
|
|
353
|
+
return cls._order_side_to_okx_map[side]
|
|
354
|
+
|
|
355
|
+
@classmethod
|
|
356
|
+
def to_okx_trigger_type(cls, trigger_type: TriggerType) -> OkxTriggerType:
|
|
357
|
+
return cls._trigger_type_to_okx_map[trigger_type]
|
|
358
|
+
|
|
359
|
+
@classmethod
|
|
360
|
+
def to_okx_order_type(
|
|
361
|
+
cls, order_type: OrderType, time_in_force: TimeInForce
|
|
362
|
+
) -> OkxOrderType:
|
|
363
|
+
if order_type == OrderType.MARKET:
|
|
364
|
+
return OkxOrderType.MARKET
|
|
365
|
+
elif order_type == OrderType.POST_ONLY:
|
|
366
|
+
return OkxOrderType.POST_ONLY
|
|
367
|
+
|
|
368
|
+
match time_in_force:
|
|
369
|
+
case TimeInForce.GTC:
|
|
370
|
+
return OkxOrderType.LIMIT # OKX limit orders are GTC by default
|
|
371
|
+
case TimeInForce.FOK:
|
|
372
|
+
return OkxOrderType.FOK
|
|
373
|
+
case TimeInForce.IOC:
|
|
374
|
+
return OkxOrderType.IOC
|
|
375
|
+
case _:
|
|
376
|
+
raise RuntimeError(
|
|
377
|
+
f"Could not determine OKX order type from order_type {order_type} and time_in_force {time_in_force}, valid OKX order types are: {list(OkxOrderType)}",
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
@classmethod
|
|
381
|
+
def to_okx_kline_interval(cls, interval: KlineInterval) -> OkxKlineInterval:
|
|
382
|
+
if interval not in cls._kline_interval_to_okx_map:
|
|
383
|
+
raise KlineSupportedError(
|
|
384
|
+
f"Kline interval {interval} is not supported by OKX"
|
|
385
|
+
)
|
|
386
|
+
return cls._kline_interval_to_okx_map[interval]
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
class OkxRateLimiter(RateLimiter):
|
|
390
|
+
def __init__(self, enable_rate_limit: bool = True):
|
|
391
|
+
self._enabled = enable_rate_limit
|
|
392
|
+
self._throttled: dict[str, Throttled] = {
|
|
393
|
+
"/api/v5/trade/order": Throttled(
|
|
394
|
+
quota=rate_limiter.per_sec(30),
|
|
395
|
+
timeout=-1,
|
|
396
|
+
using=RateLimiterType.GCRA.value,
|
|
397
|
+
),
|
|
398
|
+
"/api/v5/trade/cancel-order": Throttled(
|
|
399
|
+
quota=rate_limiter.per_sec(30),
|
|
400
|
+
timeout=-1,
|
|
401
|
+
using=RateLimiterType.GCRA.value,
|
|
402
|
+
),
|
|
403
|
+
"/api/v5/trade/amend-order": Throttled(
|
|
404
|
+
quota=rate_limiter.per_sec(30),
|
|
405
|
+
timeout=-1,
|
|
406
|
+
using=RateLimiterType.GCRA.value,
|
|
407
|
+
),
|
|
408
|
+
"/api/v5/trade/batch-orders": Throttled(
|
|
409
|
+
quota=rate_limiter.per_sec(150),
|
|
410
|
+
timeout=-1,
|
|
411
|
+
using=RateLimiterType.GCRA.value,
|
|
412
|
+
),
|
|
413
|
+
"/api/v5/trade/cancel-batch-orders": Throttled(
|
|
414
|
+
quota=rate_limiter.per_sec(150),
|
|
415
|
+
timeout=-1,
|
|
416
|
+
using=RateLimiterType.GCRA.value,
|
|
417
|
+
),
|
|
418
|
+
"/ws/order": Throttled(
|
|
419
|
+
quota=rate_limiter.per_sec(30),
|
|
420
|
+
timeout=-1,
|
|
421
|
+
using=RateLimiterType.GCRA.value,
|
|
422
|
+
),
|
|
423
|
+
"/ws/cancel": Throttled(
|
|
424
|
+
quota=rate_limiter.per_sec(30),
|
|
425
|
+
timeout=-1,
|
|
426
|
+
using=RateLimiterType.GCRA.value,
|
|
427
|
+
),
|
|
428
|
+
"/api/v5/account/balance": Throttled(
|
|
429
|
+
quota=rate_limiter.per_sec(5, burst=1),
|
|
430
|
+
timeout=-1,
|
|
431
|
+
using=RateLimiterType.GCRA.value,
|
|
432
|
+
),
|
|
433
|
+
"/api/v5/account/positions": Throttled(
|
|
434
|
+
quota=rate_limiter.per_sec(5, burst=1),
|
|
435
|
+
timeout=-1,
|
|
436
|
+
using=RateLimiterType.GCRA.value,
|
|
437
|
+
),
|
|
438
|
+
"/api/v5/market/candles": Throttled(
|
|
439
|
+
quota=rate_limiter.per_sec(20, burst=1),
|
|
440
|
+
timeout=-1,
|
|
441
|
+
using=RateLimiterType.GCRA.value,
|
|
442
|
+
),
|
|
443
|
+
"/api/v5/market/history-candles": Throttled(
|
|
444
|
+
quota=rate_limiter.per_sec(10, burst=1),
|
|
445
|
+
timeout=-1,
|
|
446
|
+
using=RateLimiterType.GCRA.value,
|
|
447
|
+
),
|
|
448
|
+
"/api/v5/account/config": Throttled(
|
|
449
|
+
quota=rate_limiter.per_sec(2, burst=1),
|
|
450
|
+
timeout=-1,
|
|
451
|
+
using=RateLimiterType.GCRA.value,
|
|
452
|
+
),
|
|
453
|
+
"/api/v5/market/history-index-candles": Throttled(
|
|
454
|
+
quota=rate_limiter.per_sec(4, burst=1),
|
|
455
|
+
timeout=-1,
|
|
456
|
+
using=RateLimiterType.GCRA.value,
|
|
457
|
+
),
|
|
458
|
+
"/api/v5/market/tickers": Throttled(
|
|
459
|
+
quota=rate_limiter.per_sec(10, burst=1),
|
|
460
|
+
timeout=-1,
|
|
461
|
+
using=RateLimiterType.GCRA.value,
|
|
462
|
+
),
|
|
463
|
+
"/api/v5/market/ticker": Throttled(
|
|
464
|
+
quota=rate_limiter.per_sec(10, burst=1),
|
|
465
|
+
timeout=-1,
|
|
466
|
+
using=RateLimiterType.GCRA.value,
|
|
467
|
+
),
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
@staticmethod
|
|
471
|
+
def _raise_if_limited(
|
|
472
|
+
result, message: str, scope: str, endpoint: str | None = None
|
|
473
|
+
):
|
|
474
|
+
if result.limited:
|
|
475
|
+
raise OkxRateLimitError(
|
|
476
|
+
message,
|
|
477
|
+
retry_after=result.state.retry_after,
|
|
478
|
+
scope=scope,
|
|
479
|
+
endpoint=endpoint,
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
async def order_limit(self, endpoint: str, cost: int = 1):
|
|
483
|
+
if not self._enabled:
|
|
484
|
+
return
|
|
485
|
+
result = await self._throttled[endpoint].limit(
|
|
486
|
+
key=endpoint, cost=cost, timeout=-1
|
|
487
|
+
)
|
|
488
|
+
self._raise_if_limited(
|
|
489
|
+
result,
|
|
490
|
+
f"OKX order rate limit exceeded: {endpoint}",
|
|
491
|
+
scope="uid",
|
|
492
|
+
endpoint=endpoint,
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
async def query_limit(self, endpoint: str, cost: int = 1):
|
|
496
|
+
if not self._enabled:
|
|
497
|
+
return
|
|
498
|
+
result = await self._throttled[endpoint].limit(
|
|
499
|
+
key=endpoint, cost=cost, timeout=1
|
|
500
|
+
)
|
|
501
|
+
self._raise_if_limited(
|
|
502
|
+
result,
|
|
503
|
+
f"OKX query rate limit exceeded: {endpoint}",
|
|
504
|
+
scope="uid",
|
|
505
|
+
endpoint=endpoint,
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def strip_uuid_hyphens(uuid_str: str) -> str:
|
|
510
|
+
"""Remove hyphens from UUID string for OKX API compatibility."""
|
|
511
|
+
return uuid_str.replace("-", "")
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def restore_uuid_hyphens(uuid_str: str) -> str:
|
|
515
|
+
"""Restore hyphens to UUID string from OKX API response."""
|
|
516
|
+
if len(uuid_str) != 32:
|
|
517
|
+
return uuid_str # Return as-is if not a valid stripped UUID
|
|
518
|
+
return f"{uuid_str[:8]}-{uuid_str[8:12]}-{uuid_str[12:16]}-{uuid_str[16:20]}-{uuid_str[20:]}"
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from typing import Dict, Literal, cast
|
|
3
|
+
from walrasquant.constants import AccountType
|
|
4
|
+
from walrasquant.schema import InstrumentId, BaseMarket
|
|
5
|
+
from walrasquant.core.cache import AsyncCache
|
|
6
|
+
from walrasquant.core.nautilius_core import MessageBus, LiveClock
|
|
7
|
+
from walrasquant.core.entity import TaskManager
|
|
8
|
+
from walrasquant.core.registry import OrderRegistry
|
|
9
|
+
from walrasquant.exchange.okx import OkxAccountType
|
|
10
|
+
from walrasquant.exchange.okx.schema import OkxMarket
|
|
11
|
+
from walrasquant.base import ExecutionManagementSystem
|
|
12
|
+
from walrasquant.base.ems import PriorityOrderQueue
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class OkxExecutionManagementSystem(ExecutionManagementSystem):
|
|
16
|
+
_market: Dict[str, OkxMarket]
|
|
17
|
+
|
|
18
|
+
OKX_ACCOUNT_TYPE_PRIORITY = [
|
|
19
|
+
OkxAccountType.DEMO,
|
|
20
|
+
# OkxAccountType.AWS,
|
|
21
|
+
OkxAccountType.LIVE,
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
market: Dict[str, OkxMarket],
|
|
27
|
+
cache: AsyncCache,
|
|
28
|
+
msgbus: MessageBus,
|
|
29
|
+
clock: LiveClock,
|
|
30
|
+
task_manager: TaskManager,
|
|
31
|
+
registry: OrderRegistry,
|
|
32
|
+
queue_maxsize: int = 100_000,
|
|
33
|
+
):
|
|
34
|
+
super().__init__(
|
|
35
|
+
market=market,
|
|
36
|
+
cache=cache,
|
|
37
|
+
msgbus=msgbus,
|
|
38
|
+
clock=clock,
|
|
39
|
+
task_manager=task_manager,
|
|
40
|
+
registry=registry,
|
|
41
|
+
queue_maxsize=queue_maxsize,
|
|
42
|
+
)
|
|
43
|
+
self._okx_account_type: OkxAccountType | None = None
|
|
44
|
+
|
|
45
|
+
def _build_order_submit_queues(self):
|
|
46
|
+
for account_type in self._private_connectors.keys():
|
|
47
|
+
if isinstance(account_type, OkxAccountType):
|
|
48
|
+
self._order_submit_queues[account_type] = PriorityOrderQueue(
|
|
49
|
+
maxsize=self._queue_maxsize
|
|
50
|
+
)
|
|
51
|
+
break
|
|
52
|
+
|
|
53
|
+
def _set_account_type(self):
|
|
54
|
+
account_types = self._private_connectors.keys()
|
|
55
|
+
for account_type in self.OKX_ACCOUNT_TYPE_PRIORITY:
|
|
56
|
+
if account_type in account_types:
|
|
57
|
+
self._okx_account_type = account_type
|
|
58
|
+
break
|
|
59
|
+
|
|
60
|
+
def _instrument_id_to_account_type(
|
|
61
|
+
self, instrument_id: InstrumentId
|
|
62
|
+
) -> AccountType:
|
|
63
|
+
if self._okx_account_type is None:
|
|
64
|
+
raise ValueError("No OKX account type configured")
|
|
65
|
+
return self._okx_account_type
|
|
66
|
+
|
|
67
|
+
def _get_min_order_amount(
|
|
68
|
+
self, symbol: str, market: BaseMarket, px: float
|
|
69
|
+
) -> Decimal:
|
|
70
|
+
okx_market = cast(OkxMarket, market)
|
|
71
|
+
amount_limits = okx_market.limits.amount
|
|
72
|
+
min_order_amount = amount_limits.min if amount_limits is not None else 0.0
|
|
73
|
+
min_order_amount = min_order_amount or 0.0
|
|
74
|
+
min_order_amount = super()._amount_to_precision(
|
|
75
|
+
symbol, min_order_amount, mode="ceil"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if not okx_market.spot:
|
|
79
|
+
# for linear and inverse, the min order amount is contract size and ctVal is base amount per contract
|
|
80
|
+
min_order_amount *= Decimal(okx_market.info.ctVal or "1")
|
|
81
|
+
|
|
82
|
+
return min_order_amount
|
|
83
|
+
|
|
84
|
+
# override the base method
|
|
85
|
+
def _amount_to_precision(
|
|
86
|
+
self,
|
|
87
|
+
symbol: str,
|
|
88
|
+
amount: float,
|
|
89
|
+
mode: Literal["round"] | Literal["ceil"] | Literal["floor"] = "round",
|
|
90
|
+
) -> Decimal:
|
|
91
|
+
market = self._market[symbol]
|
|
92
|
+
ctVal = Decimal("1")
|
|
93
|
+
if not market.spot:
|
|
94
|
+
ctVal = Decimal(market.info.ctVal or "1")
|
|
95
|
+
adjusted_amount = float(Decimal(str(amount)) / ctVal)
|
|
96
|
+
return super()._amount_to_precision(symbol, adjusted_amount, mode) * ctVal
|
|
97
|
+
return super()._amount_to_precision(symbol, amount, mode)
|
|
98
|
+
|
|
99
|
+
def _get_max_order_amount(
|
|
100
|
+
self, symbol: str, market: BaseMarket, is_market: bool, px: float
|
|
101
|
+
) -> Decimal:
|
|
102
|
+
# maxLmtSz String The maximum order quantity of a single limit order. # If it is a derivatives contract, the value is the number of contracts. # If it is SPOT/MARGIN, the value is the quantity in base currency.
|
|
103
|
+
# maxMktSz String The maximum order quantity of a single market order. # If it is a derivatives contract, the value is the number of contracts. # If it is SPOT/MARGIN, the value is the quantity in USDT.
|
|
104
|
+
# maxLmtAmt String Max USD amount for a single limit order
|
|
105
|
+
# maxMktAmt String Max USD amount for a single market order # Only applicable to SPOT/MARGIN
|
|
106
|
+
okx_market = cast(OkxMarket, market)
|
|
107
|
+
ctVal = Decimal(okx_market.info.ctVal or "1")
|
|
108
|
+
if is_market:
|
|
109
|
+
maxMktSz = okx_market.info.maxMktSz or "Infinity"
|
|
110
|
+
maxMktAmt = okx_market.info.maxMktAmt or "Infinity"
|
|
111
|
+
|
|
112
|
+
if not okx_market.spot:
|
|
113
|
+
return Decimal(maxMktSz) * ctVal
|
|
114
|
+
else:
|
|
115
|
+
_MinMktSzAmt = min(float(maxMktSz), float(maxMktAmt))
|
|
116
|
+
return self._amount_to_precision(
|
|
117
|
+
symbol, _MinMktSzAmt / px, mode="floor"
|
|
118
|
+
)
|
|
119
|
+
else:
|
|
120
|
+
maxLmtSz = (
|
|
121
|
+
okx_market.info.maxLmtSz or "Infinity"
|
|
122
|
+
) # SPOT/MARGIN: quantity in base currency, DERIVATIVE: number of contracts
|
|
123
|
+
maxLmtAmt = okx_market.info.maxLmtAmt
|
|
124
|
+
|
|
125
|
+
if not okx_market.spot:
|
|
126
|
+
_maxLmtSz = Decimal(maxLmtSz) * ctVal
|
|
127
|
+
_maxLmtAmt = (
|
|
128
|
+
self._amount_to_precision(
|
|
129
|
+
symbol, float(maxLmtAmt) / px, mode="floor"
|
|
130
|
+
)
|
|
131
|
+
if maxLmtAmt
|
|
132
|
+
else Decimal("Infinity")
|
|
133
|
+
)
|
|
134
|
+
return min(_maxLmtSz, _maxLmtAmt)
|
|
135
|
+
else:
|
|
136
|
+
_maxLmtSz = Decimal(maxLmtSz)
|
|
137
|
+
_maxLmtAmt = (
|
|
138
|
+
self._amount_to_precision(
|
|
139
|
+
symbol, float(maxLmtAmt) / px, mode="floor"
|
|
140
|
+
)
|
|
141
|
+
if maxLmtAmt
|
|
142
|
+
else Decimal("Infinity")
|
|
143
|
+
)
|
|
144
|
+
return min(_maxLmtSz, _maxLmtAmt)
|