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/schema.py
ADDED
|
@@ -0,0 +1,755 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
from collections import defaultdict
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from typing import Dict, List, Any
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from msgspec import Struct, field
|
|
7
|
+
from walrasquant.core.nautilius_core import UUID4
|
|
8
|
+
from walrasquant.constants import (
|
|
9
|
+
# AccountType,
|
|
10
|
+
OrderSide,
|
|
11
|
+
OrderType,
|
|
12
|
+
TimeInForce,
|
|
13
|
+
OrderStatus,
|
|
14
|
+
PositionSide,
|
|
15
|
+
InstrumentType,
|
|
16
|
+
ExchangeType,
|
|
17
|
+
KlineInterval,
|
|
18
|
+
TriggerType,
|
|
19
|
+
DataType,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class BatchOrder(Struct, kw_only=True):
|
|
24
|
+
symbol: str
|
|
25
|
+
side: OrderSide
|
|
26
|
+
type: OrderType
|
|
27
|
+
amount: Decimal
|
|
28
|
+
price: Decimal | None = None # for market order, the price is None
|
|
29
|
+
time_in_force: TimeInForce | None = TimeInForce.GTC
|
|
30
|
+
reduce_only: bool = False
|
|
31
|
+
kwargs: Dict[str, Any] = field(default_factory=dict)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CancelBatchOrder(Struct, kw_only=True):
|
|
35
|
+
symbol: str
|
|
36
|
+
oid: str
|
|
37
|
+
kwargs: Dict[str, Any] = field(default_factory=dict)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Symbol(str):
|
|
41
|
+
"""Symbol class that inherits from string with all InstrumentId methods."""
|
|
42
|
+
|
|
43
|
+
def __new__(cls, value: str):
|
|
44
|
+
instance = str.__new__(cls, value)
|
|
45
|
+
instance._instrument_id = None
|
|
46
|
+
return instance
|
|
47
|
+
|
|
48
|
+
def __init__(self, value: str):
|
|
49
|
+
super().__init__()
|
|
50
|
+
self._instrument_id = InstrumentId.from_str(value)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def is_spot(self) -> bool:
|
|
54
|
+
return self._instrument_id.is_spot
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def is_linear(self) -> bool:
|
|
58
|
+
return self._instrument_id.is_linear
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def is_inverse(self) -> bool:
|
|
62
|
+
return self._instrument_id.is_inverse
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def id(self) -> str:
|
|
66
|
+
"""Get the id from the symbol."""
|
|
67
|
+
return self._instrument_id.id
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def exchange(self) -> ExchangeType:
|
|
71
|
+
"""Get the exchange from the symbol."""
|
|
72
|
+
return self._instrument_id.exchange
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def type(self) -> InstrumentType:
|
|
76
|
+
"""Get the instrument type from the symbol."""
|
|
77
|
+
return self._instrument_id.type
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class InstrumentId(Struct):
|
|
81
|
+
id: str
|
|
82
|
+
symbol: str
|
|
83
|
+
exchange: ExchangeType
|
|
84
|
+
type: InstrumentType
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def is_spot(self) -> bool:
|
|
88
|
+
return self.type == InstrumentType.SPOT
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def is_linear(self) -> bool:
|
|
92
|
+
return self.type == InstrumentType.LINEAR
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def is_inverse(self) -> bool:
|
|
96
|
+
return self.type == InstrumentType.INVERSE
|
|
97
|
+
|
|
98
|
+
@classmethod
|
|
99
|
+
def from_str(cls, symbol: str):
|
|
100
|
+
"""
|
|
101
|
+
BTCETH.BINANCE -> SPOT
|
|
102
|
+
BTCUSDT-PERP.BINANCE -> LINEAR
|
|
103
|
+
BTCUSD.BINANCE -> INVERSE
|
|
104
|
+
BTCUSD-241227.BINANCE
|
|
105
|
+
"""
|
|
106
|
+
symbol_prefix, exchange = symbol.split(".")
|
|
107
|
+
|
|
108
|
+
# if numirical number in id, then it is a future
|
|
109
|
+
if "-" in symbol_prefix:
|
|
110
|
+
prefix, _ = symbol_prefix.split("-")
|
|
111
|
+
if prefix.endswith("USD"):
|
|
112
|
+
type = InstrumentType.INVERSE
|
|
113
|
+
else:
|
|
114
|
+
type = InstrumentType.LINEAR
|
|
115
|
+
else:
|
|
116
|
+
type = InstrumentType.SPOT
|
|
117
|
+
|
|
118
|
+
return cls(
|
|
119
|
+
id=symbol_prefix,
|
|
120
|
+
symbol=symbol,
|
|
121
|
+
exchange=ExchangeType(exchange.lower()),
|
|
122
|
+
type=type,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class BookL1(Struct, gc=False, frozen=True):
|
|
127
|
+
exchange: ExchangeType
|
|
128
|
+
symbol: str
|
|
129
|
+
bid: float
|
|
130
|
+
ask: float
|
|
131
|
+
bid_size: float
|
|
132
|
+
ask_size: float
|
|
133
|
+
timestamp: int
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def mid(self) -> float:
|
|
137
|
+
return (self.bid + self.ask) / 2
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def spread(self) -> float:
|
|
141
|
+
return self.ask - self.bid
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def weighted_mid(self) -> float:
|
|
145
|
+
return (self.bid * self.ask_size + self.ask * self.bid_size) / (
|
|
146
|
+
self.ask_size + self.bid_size
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class BookOrderData(Struct, gc=False, frozen=True):
|
|
151
|
+
price: float
|
|
152
|
+
size: float
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class BookL2(Struct, frozen=True):
|
|
156
|
+
exchange: ExchangeType
|
|
157
|
+
symbol: str
|
|
158
|
+
bids: List[BookOrderData] # desc order
|
|
159
|
+
asks: List[BookOrderData] # asc order
|
|
160
|
+
timestamp: int
|
|
161
|
+
|
|
162
|
+
@property
|
|
163
|
+
def mid(self) -> float:
|
|
164
|
+
if not self.bids or not self.asks:
|
|
165
|
+
return 0
|
|
166
|
+
return (self.bids[0].price + self.asks[0].price) / 2
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def spread(self) -> float:
|
|
170
|
+
if not self.bids or not self.asks:
|
|
171
|
+
return 0
|
|
172
|
+
return self.asks[0].price - self.bids[0].price
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def best_bid(self) -> float:
|
|
176
|
+
if not self.bids:
|
|
177
|
+
return 0
|
|
178
|
+
return self.bids[0].price
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def best_ask(self) -> float:
|
|
182
|
+
if not self.asks:
|
|
183
|
+
return 0
|
|
184
|
+
return self.asks[0].price
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def best_bid_size(self) -> float:
|
|
188
|
+
if not self.bids:
|
|
189
|
+
return 0
|
|
190
|
+
return self.bids[0].size
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def best_ask_size(self) -> float:
|
|
194
|
+
if not self.asks:
|
|
195
|
+
return 0
|
|
196
|
+
return self.asks[0].size
|
|
197
|
+
|
|
198
|
+
# Weighted-Depth Order Book Price
|
|
199
|
+
# Be aware that price and quantity are multiplied within the same side
|
|
200
|
+
@property
|
|
201
|
+
def weighted_mid(self) -> float:
|
|
202
|
+
bid_total = sum([bid.price * bid.size for bid in self.bids])
|
|
203
|
+
ask_total = sum([ask.price * ask.size for ask in self.asks])
|
|
204
|
+
total_size = sum([bid.size for bid in self.bids]) + sum(
|
|
205
|
+
[ask.size for ask in self.asks]
|
|
206
|
+
)
|
|
207
|
+
return (bid_total + ask_total) / total_size
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class Trade(Struct, gc=False, frozen=True):
|
|
211
|
+
exchange: ExchangeType
|
|
212
|
+
side: OrderSide
|
|
213
|
+
symbol: str
|
|
214
|
+
price: float
|
|
215
|
+
size: float
|
|
216
|
+
timestamp: int
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
class Kline(Struct, gc=False, kw_only=True, frozen=True, omit_defaults=True):
|
|
220
|
+
exchange: ExchangeType
|
|
221
|
+
symbol: str
|
|
222
|
+
interval: KlineInterval
|
|
223
|
+
open: float
|
|
224
|
+
high: float
|
|
225
|
+
low: float
|
|
226
|
+
close: float
|
|
227
|
+
volume: float | None = None
|
|
228
|
+
buy_volume: float | None = None # for trade aggregation only add trade.side == BUY
|
|
229
|
+
quote_volume: float | None = None # only for binance and okx
|
|
230
|
+
taker_volume: float | None = None # only for binance
|
|
231
|
+
taker_quote_volume: float | None = None # only for binance
|
|
232
|
+
turnover: float | None = None # only for bybit
|
|
233
|
+
start: int
|
|
234
|
+
timestamp: int
|
|
235
|
+
confirm: bool
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
class MarkPrice(Struct, gc=False, frozen=True):
|
|
239
|
+
exchange: ExchangeType
|
|
240
|
+
symbol: str
|
|
241
|
+
price: float
|
|
242
|
+
timestamp: int
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
class FundingRate(Struct, gc=False, frozen=True):
|
|
246
|
+
exchange: ExchangeType
|
|
247
|
+
symbol: str
|
|
248
|
+
rate: float
|
|
249
|
+
timestamp: int
|
|
250
|
+
next_funding_time: int
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class Ticker(Struct, gc=False, kw_only=True):
|
|
254
|
+
"""
|
|
255
|
+
Universal ticker data structure supporting all exchanges.
|
|
256
|
+
volumeCcy: for linear and spot markets, it is the volume in quote currency.
|
|
257
|
+
For inverse markets, it is the volume in base currency.
|
|
258
|
+
"""
|
|
259
|
+
|
|
260
|
+
exchange: ExchangeType
|
|
261
|
+
symbol: str
|
|
262
|
+
last_price: float
|
|
263
|
+
timestamp: int
|
|
264
|
+
volume: float
|
|
265
|
+
volumeCcy: float
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class IndexPrice(Struct, gc=False, frozen=True):
|
|
269
|
+
exchange: ExchangeType
|
|
270
|
+
symbol: str
|
|
271
|
+
price: float
|
|
272
|
+
timestamp: int
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class OrderSubmit(Struct):
|
|
276
|
+
symbol: str
|
|
277
|
+
instrument_id: InstrumentId
|
|
278
|
+
kwargs: Dict[str, Any] = field(default_factory=dict)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
class BatchOrderSubmit(OrderSubmit, kw_only=True):
|
|
282
|
+
oid: str
|
|
283
|
+
side: OrderSide
|
|
284
|
+
type: OrderType
|
|
285
|
+
amount: Decimal
|
|
286
|
+
price: Decimal | None = None # for market order, the price is None
|
|
287
|
+
time_in_force: TimeInForce = TimeInForce.GTC
|
|
288
|
+
reduce_only: bool = False
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
class CreateOrderSubmit(OrderSubmit, kw_only=True):
|
|
292
|
+
oid: str
|
|
293
|
+
side: OrderSide
|
|
294
|
+
type: OrderType
|
|
295
|
+
amount: Decimal
|
|
296
|
+
price: Decimal | None = None # for market order, the price is None
|
|
297
|
+
time_in_force: TimeInForce | None = TimeInForce.GTC
|
|
298
|
+
reduce_only: bool = False
|
|
299
|
+
# position_side: PositionSide | None = None
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class CancelOrderSubmit(OrderSubmit, kw_only=True):
|
|
303
|
+
oid: str
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class CancelAllOrderSubmit(OrderSubmit, kw_only=True):
|
|
307
|
+
pass
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
class TakeProfitAndStopLossOrderSubmit(CreateOrderSubmit, kw_only=True):
|
|
311
|
+
tp_order_type: OrderType
|
|
312
|
+
tp_trigger_price: Decimal | None = None
|
|
313
|
+
tp_price: Decimal | None = None
|
|
314
|
+
tp_trigger_type: TriggerType = TriggerType.LAST_PRICE
|
|
315
|
+
sl_order_type: OrderType
|
|
316
|
+
sl_trigger_price: Decimal | None = None
|
|
317
|
+
sl_price: Decimal | None = None
|
|
318
|
+
sl_trigger_type: TriggerType = TriggerType.LAST_PRICE
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class TWAPOrderSubmit(OrderSubmit, kw_only=True):
|
|
322
|
+
uuid: str = field(default_factory=lambda: f"ALGO-{UUID4().value}")
|
|
323
|
+
side: OrderSide
|
|
324
|
+
amount: Decimal
|
|
325
|
+
duration: int
|
|
326
|
+
wait: int
|
|
327
|
+
position_side: PositionSide
|
|
328
|
+
check_interval: float = 0.1
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
class CancelTWAPOrderSubmit(OrderSubmit, kw_only=True):
|
|
332
|
+
uuid: str
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class ModifyOrderSubmit(OrderSubmit, kw_only=True):
|
|
336
|
+
oid: str
|
|
337
|
+
side: OrderSide
|
|
338
|
+
price: Decimal
|
|
339
|
+
amount: Decimal
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
class SubscriptionSubmit(Struct):
|
|
343
|
+
symbols: List[str]
|
|
344
|
+
data_type: DataType
|
|
345
|
+
params: Dict[str, Any] = field(default_factory=dict)
|
|
346
|
+
ready_timeout: int = 60
|
|
347
|
+
ready: bool = True
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
class UnsubscriptionSubmit(Struct):
|
|
351
|
+
symbols: List[str]
|
|
352
|
+
data_type: DataType
|
|
353
|
+
params: Dict[str, Any] = field(default_factory=dict)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
class Order(Struct):
|
|
357
|
+
exchange: ExchangeType
|
|
358
|
+
symbol: str
|
|
359
|
+
status: OrderStatus
|
|
360
|
+
oid: Optional[str] = None
|
|
361
|
+
eid: Optional[str] = None
|
|
362
|
+
amount: Optional[Decimal] = None
|
|
363
|
+
filled: Optional[Decimal] = None
|
|
364
|
+
timestamp: Optional[int] = None
|
|
365
|
+
type: Optional[OrderType] = None
|
|
366
|
+
side: Optional[OrderSide] = None
|
|
367
|
+
time_in_force: Optional[TimeInForce] = None
|
|
368
|
+
price: Optional[float] = None
|
|
369
|
+
trigger_price: Optional[float] = None
|
|
370
|
+
average: Optional[float] = None
|
|
371
|
+
last_filled_price: Optional[float] = None
|
|
372
|
+
last_filled: Optional[Decimal] = None
|
|
373
|
+
remaining: Optional[Decimal] = None
|
|
374
|
+
fee: Optional[Decimal] = None
|
|
375
|
+
fee_currency: Optional[str] = None
|
|
376
|
+
cost: Optional[Decimal] = None
|
|
377
|
+
cum_cost: Optional[Decimal] = None
|
|
378
|
+
reduce_only: Optional[bool] = None
|
|
379
|
+
position_side: Optional[PositionSide] = None
|
|
380
|
+
reason: Optional[str] = None
|
|
381
|
+
|
|
382
|
+
@property
|
|
383
|
+
def success(self) -> bool:
|
|
384
|
+
return self.status not in [OrderStatus.FAILED, OrderStatus.CANCEL_FAILED]
|
|
385
|
+
|
|
386
|
+
@property
|
|
387
|
+
def is_filled(self) -> bool:
|
|
388
|
+
return self.status == OrderStatus.FILLED
|
|
389
|
+
|
|
390
|
+
@property
|
|
391
|
+
def is_pending(self) -> bool:
|
|
392
|
+
return self.status == OrderStatus.PENDING
|
|
393
|
+
|
|
394
|
+
@property
|
|
395
|
+
def is_accepted(self) -> bool:
|
|
396
|
+
return self.status == OrderStatus.ACCEPTED
|
|
397
|
+
|
|
398
|
+
@property
|
|
399
|
+
def is_partially_filled(self) -> bool:
|
|
400
|
+
return self.status == OrderStatus.PARTIALLY_FILLED
|
|
401
|
+
|
|
402
|
+
@property
|
|
403
|
+
def is_partially_canceled(self) -> bool:
|
|
404
|
+
return self.status == OrderStatus.CANCELED and (
|
|
405
|
+
self.filled or Decimal("0")
|
|
406
|
+
) > Decimal("0")
|
|
407
|
+
|
|
408
|
+
@property
|
|
409
|
+
def is_canceling(self) -> bool:
|
|
410
|
+
return self.status == OrderStatus.CANCELING
|
|
411
|
+
|
|
412
|
+
@property
|
|
413
|
+
def is_cancel_failed(self) -> bool:
|
|
414
|
+
return self.status == OrderStatus.CANCEL_FAILED
|
|
415
|
+
|
|
416
|
+
@property
|
|
417
|
+
def is_canceled(self) -> bool:
|
|
418
|
+
return self.status == OrderStatus.CANCELED
|
|
419
|
+
|
|
420
|
+
@property
|
|
421
|
+
def is_expired(self) -> bool:
|
|
422
|
+
return self.status == OrderStatus.EXPIRED
|
|
423
|
+
|
|
424
|
+
@property
|
|
425
|
+
def is_closed(self) -> bool:
|
|
426
|
+
return self.status in [
|
|
427
|
+
OrderStatus.FILLED,
|
|
428
|
+
OrderStatus.CANCELED,
|
|
429
|
+
OrderStatus.EXPIRED,
|
|
430
|
+
OrderStatus.FAILED,
|
|
431
|
+
]
|
|
432
|
+
|
|
433
|
+
@property
|
|
434
|
+
def is_opened(self) -> bool:
|
|
435
|
+
return self.status in [
|
|
436
|
+
OrderStatus.PENDING,
|
|
437
|
+
OrderStatus.CANCELING,
|
|
438
|
+
OrderStatus.PARTIALLY_FILLED,
|
|
439
|
+
OrderStatus.ACCEPTED,
|
|
440
|
+
]
|
|
441
|
+
|
|
442
|
+
@property
|
|
443
|
+
def on_flight(self) -> bool:
|
|
444
|
+
return self.status in [
|
|
445
|
+
OrderStatus.PENDING,
|
|
446
|
+
OrderStatus.CANCELING,
|
|
447
|
+
]
|
|
448
|
+
|
|
449
|
+
@property
|
|
450
|
+
def is_buy(self) -> bool:
|
|
451
|
+
return self.side == OrderSide.BUY
|
|
452
|
+
|
|
453
|
+
@property
|
|
454
|
+
def is_sell(self) -> bool:
|
|
455
|
+
return self.side == OrderSide.SELL
|
|
456
|
+
|
|
457
|
+
@property
|
|
458
|
+
def is_maker(self) -> bool:
|
|
459
|
+
return self.type == OrderType.LIMIT
|
|
460
|
+
|
|
461
|
+
@property
|
|
462
|
+
def is_taker(self) -> bool:
|
|
463
|
+
return self.type == OrderType.MARKET
|
|
464
|
+
|
|
465
|
+
@property
|
|
466
|
+
def is_post_only(self) -> bool:
|
|
467
|
+
return self.type == OrderType.POST_ONLY
|
|
468
|
+
|
|
469
|
+
@property
|
|
470
|
+
def is_ioc(self) -> bool:
|
|
471
|
+
return self.time_in_force == TimeInForce.IOC
|
|
472
|
+
|
|
473
|
+
@property
|
|
474
|
+
def is_fok(self) -> bool:
|
|
475
|
+
return self.time_in_force == TimeInForce.FOK
|
|
476
|
+
|
|
477
|
+
@property
|
|
478
|
+
def is_gtc(self) -> bool:
|
|
479
|
+
return self.time_in_force == TimeInForce.GTC
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
class Balance(Struct):
|
|
483
|
+
"""
|
|
484
|
+
Buy BTC/USDT: amount = 0.01, cost: 600
|
|
485
|
+
|
|
486
|
+
OrderStatus.INITIALIZED: BTC(free: 0.0, locked: 0.0) USDT(free: 1000, locked: 0)
|
|
487
|
+
OrderStatus.PENDING: BTC(free: 0.0, locked: 0) USDT(free: 400, locked: 600) USDT.update_locked(600) USDT.update_free(-600)
|
|
488
|
+
|
|
489
|
+
OrderStatus.PARTIALLY_FILLED: BTC(free: 0.005, locked: 0) USDT(free: 400, locked: 300) BTC.update_free(0.005) USDT.update_locked(-300)
|
|
490
|
+
OrderStatus.FILLED: BTC(free: 0.01, locked: 0.0) USDT(free: 400, locked: 0) BTC.update_free(0.005) USDT.update_locked(-300)
|
|
491
|
+
|
|
492
|
+
Buy BTC/USDT: amount = 0.01, cost: 200
|
|
493
|
+
|
|
494
|
+
OrderStatus.INITIALIZED: BTC(free: 0.01, locked: 0.0) USDT(free: 400, locked: 0)
|
|
495
|
+
OrderStatus.PENDING: BTC(free: 0.01, locked: 0.0) USDT(free: 200, locked: 200) USDT.update_locked(200) USDT.update_free(-200)
|
|
496
|
+
OrderStatus.FILLED: BTC(free: 0.02, locked: 0.0) USDT(free: 200, locked: 0) BTC.update_free(0.01) USDT.update_locked(-200)
|
|
497
|
+
|
|
498
|
+
Sell BTC/USDT: amount = 0.01, cost: 300
|
|
499
|
+
OrderStatus.INITIALIZED: BTC(free: 0.02, locked: 0.0) USDT(free: 200, locked: 0)
|
|
500
|
+
OrderStatus.PENDING: BTC(free: 0.01, locked: 0.01) USDT(free: 200, locked: 0) BTC.update_locked(0.01) BTC.update_free(-0.01)
|
|
501
|
+
OrderStatus.PARTIALLY_FILLED: BTC(free: 0.01, locked: 0.005) USDT(free: 350, locked: 0) BTC.update_locked(-0.005) USDT.update_free(150)
|
|
502
|
+
OrderStatus.FILLED: BTC(free: 0.01, locked: 0.0) USDT(free: 500, locked: 0) BTC.update_locked(-0.005) USDT.update_free(150)
|
|
503
|
+
"""
|
|
504
|
+
|
|
505
|
+
asset: str
|
|
506
|
+
free: Decimal = field(default=Decimal("0.0"))
|
|
507
|
+
locked: Decimal = field(default=Decimal("0.0"))
|
|
508
|
+
|
|
509
|
+
@property
|
|
510
|
+
def total(self) -> Decimal:
|
|
511
|
+
return self.free + self.locked
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
class AccountBalance:
|
|
515
|
+
__slots__ = ("balances", "_dirty", "_cache_total", "_cache_free", "_cache_locked")
|
|
516
|
+
|
|
517
|
+
def __init__(self):
|
|
518
|
+
self.balances: Dict[str, Balance] = {}
|
|
519
|
+
self._dirty: bool = True
|
|
520
|
+
self._cache_total: Dict[str, Decimal] = {}
|
|
521
|
+
self._cache_free: Dict[str, Decimal] = {}
|
|
522
|
+
self._cache_locked: Dict[str, Decimal] = {}
|
|
523
|
+
|
|
524
|
+
def _apply(self, balances: List[Balance]):
|
|
525
|
+
for balance in balances:
|
|
526
|
+
self.balances[balance.asset] = balance
|
|
527
|
+
self._dirty = True
|
|
528
|
+
|
|
529
|
+
def _update_free(self, asset: str, amount: Decimal):
|
|
530
|
+
if asset not in self.balances:
|
|
531
|
+
raise ValueError(f"Asset {asset} not found in balances")
|
|
532
|
+
self.balances[asset].free += amount
|
|
533
|
+
self._dirty = True
|
|
534
|
+
|
|
535
|
+
def _update_locked(self, asset: str, amount: Decimal):
|
|
536
|
+
if asset not in self.balances:
|
|
537
|
+
raise ValueError(f"Asset {asset} not found in balances")
|
|
538
|
+
self.balances[asset].locked += amount
|
|
539
|
+
self._dirty = True
|
|
540
|
+
|
|
541
|
+
def _rebuild(self):
|
|
542
|
+
total: Dict[str, Decimal] = defaultdict(Decimal)
|
|
543
|
+
free: Dict[str, Decimal] = defaultdict(Decimal)
|
|
544
|
+
locked: Dict[str, Decimal] = defaultdict(Decimal)
|
|
545
|
+
for asset, bal in self.balances.items():
|
|
546
|
+
t, f, lk = bal.total, bal.free, bal.locked
|
|
547
|
+
total[asset] = t
|
|
548
|
+
free[asset] = f
|
|
549
|
+
locked[asset] = lk
|
|
550
|
+
if "." in asset:
|
|
551
|
+
base = asset.split(".")[0]
|
|
552
|
+
total[base] += t
|
|
553
|
+
free[base] += f
|
|
554
|
+
locked[base] += lk
|
|
555
|
+
self._cache_total = total
|
|
556
|
+
self._cache_free = free
|
|
557
|
+
self._cache_locked = locked
|
|
558
|
+
self._dirty = False
|
|
559
|
+
|
|
560
|
+
@property
|
|
561
|
+
def balance_total(self) -> Dict[str, Decimal]:
|
|
562
|
+
if self._dirty:
|
|
563
|
+
self._rebuild()
|
|
564
|
+
return self._cache_total
|
|
565
|
+
|
|
566
|
+
@property
|
|
567
|
+
def balance_free(self) -> Dict[str, Decimal]:
|
|
568
|
+
if self._dirty:
|
|
569
|
+
self._rebuild()
|
|
570
|
+
return self._cache_free
|
|
571
|
+
|
|
572
|
+
@property
|
|
573
|
+
def balance_locked(self) -> Dict[str, Decimal]:
|
|
574
|
+
if self._dirty:
|
|
575
|
+
self._rebuild()
|
|
576
|
+
return self._cache_locked
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
class Precision(Struct):
|
|
580
|
+
"""
|
|
581
|
+
"precision": {
|
|
582
|
+
"amount": 0.0001,
|
|
583
|
+
"price": 1e-05,
|
|
584
|
+
"cost": null,
|
|
585
|
+
"base": 1e-08,
|
|
586
|
+
"quote": 1e-08
|
|
587
|
+
},
|
|
588
|
+
"""
|
|
589
|
+
|
|
590
|
+
amount: float
|
|
591
|
+
price: float
|
|
592
|
+
cost: float | None = None
|
|
593
|
+
base: float | None = None
|
|
594
|
+
quote: float | None = None
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
class LimitMinMax(Struct):
|
|
598
|
+
"""
|
|
599
|
+
"limits": {
|
|
600
|
+
"amount": {
|
|
601
|
+
"min": 0.0001,
|
|
602
|
+
"max": 1000.0
|
|
603
|
+
},
|
|
604
|
+
"price": {
|
|
605
|
+
"min": 1e-05,
|
|
606
|
+
"max": 1000000.0
|
|
607
|
+
},
|
|
608
|
+
"cost": {
|
|
609
|
+
"min": 0.01,
|
|
610
|
+
"max": 1000000.0
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
"""
|
|
614
|
+
|
|
615
|
+
min: float | None
|
|
616
|
+
max: float | None
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
class Limit(Struct):
|
|
620
|
+
leverage: LimitMinMax
|
|
621
|
+
amount: LimitMinMax
|
|
622
|
+
price: LimitMinMax
|
|
623
|
+
cost: LimitMinMax
|
|
624
|
+
market: LimitMinMax | None = None
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
class MarginMode(Struct):
|
|
628
|
+
isolated: bool | None
|
|
629
|
+
cross: bool | None
|
|
630
|
+
|
|
631
|
+
|
|
632
|
+
class BaseMarket(Struct):
|
|
633
|
+
"""Base market structure for all exchanges."""
|
|
634
|
+
|
|
635
|
+
id: str
|
|
636
|
+
lowercaseId: str | None
|
|
637
|
+
symbol: str
|
|
638
|
+
base: str
|
|
639
|
+
quote: str
|
|
640
|
+
settle: str | None
|
|
641
|
+
baseId: str
|
|
642
|
+
quoteId: str
|
|
643
|
+
settleId: str | None
|
|
644
|
+
type: InstrumentType
|
|
645
|
+
spot: bool
|
|
646
|
+
margin: bool | None
|
|
647
|
+
swap: bool
|
|
648
|
+
future: bool
|
|
649
|
+
option: bool
|
|
650
|
+
index: bool | str | None
|
|
651
|
+
active: bool
|
|
652
|
+
contract: bool
|
|
653
|
+
linear: bool | None
|
|
654
|
+
inverse: bool | None
|
|
655
|
+
subType: InstrumentType | None
|
|
656
|
+
taker: float
|
|
657
|
+
maker: float
|
|
658
|
+
contractSize: float | None
|
|
659
|
+
expiry: int | None
|
|
660
|
+
expiryDatetime: str | None
|
|
661
|
+
strike: float | str | None
|
|
662
|
+
optionType: str | None
|
|
663
|
+
precision: Precision
|
|
664
|
+
limits: Limit
|
|
665
|
+
marginModes: MarginMode
|
|
666
|
+
created: int | None
|
|
667
|
+
tierBased: bool | None
|
|
668
|
+
percentage: bool | None
|
|
669
|
+
# feeSide: str # not supported by okx exchanges
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
"""
|
|
673
|
+
class Position(Struct):
|
|
674
|
+
|
|
675
|
+
one-way mode:
|
|
676
|
+
> order (side: buy) -> side: buy | pos_side: net/both | reduce_only: False [open long position]
|
|
677
|
+
> order (side: sell) -> side: sell | pos_side: net/both | reduce_only: False [open short position]
|
|
678
|
+
> order (side: buy, reduce_only=True) -> side: buy | pos_side: net/both | reduce_only: True [close short position]
|
|
679
|
+
> order (side: sell, reduce_only=True) -> side: sell | pos_side: net/both | reduce_only: True [close long position]
|
|
680
|
+
|
|
681
|
+
hedge mode:
|
|
682
|
+
> order (side: buy, pos_side: long) -> side: buy | pos_side: long | reduce_only: False [open long position]
|
|
683
|
+
> order (side: sell, pos_side: short) -> side: sell | pos_side: short | reduce_only: False [open short position]
|
|
684
|
+
> order (side: sell, pos_side: long) -> side: sell | pos_side: long | reduce_only: True [close long position]
|
|
685
|
+
> order (side: buy, pos_side: short) -> side: buy | pos_side: short | reduce_only: True [close short position]
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
"""
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
class Position(Struct):
|
|
692
|
+
symbol: str
|
|
693
|
+
exchange: ExchangeType
|
|
694
|
+
signed_amount: Decimal = Decimal("0")
|
|
695
|
+
entry_price: float = 0
|
|
696
|
+
side: Optional[PositionSide] = None
|
|
697
|
+
unrealized_pnl: float = 0
|
|
698
|
+
realized_pnl: float = 0
|
|
699
|
+
|
|
700
|
+
@property
|
|
701
|
+
def amount(self) -> Decimal:
|
|
702
|
+
return abs(self.signed_amount)
|
|
703
|
+
|
|
704
|
+
@property
|
|
705
|
+
def is_opened(self) -> bool:
|
|
706
|
+
return self.amount != Decimal("0")
|
|
707
|
+
|
|
708
|
+
@property
|
|
709
|
+
def is_closed(self) -> bool:
|
|
710
|
+
return not self.is_opened
|
|
711
|
+
|
|
712
|
+
@property
|
|
713
|
+
def is_long(self) -> bool:
|
|
714
|
+
return self.side == PositionSide.LONG
|
|
715
|
+
|
|
716
|
+
@property
|
|
717
|
+
def is_short(self) -> bool:
|
|
718
|
+
return self.side == PositionSide.SHORT
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
class KlineList(list[Kline]):
|
|
722
|
+
def __init__(self, klines: list[Kline], fields: list[str] | None = None):
|
|
723
|
+
super().__init__(klines)
|
|
724
|
+
self._fields = fields or [
|
|
725
|
+
"timestamp",
|
|
726
|
+
"symbol",
|
|
727
|
+
"open",
|
|
728
|
+
"high",
|
|
729
|
+
"low",
|
|
730
|
+
"close",
|
|
731
|
+
"volume",
|
|
732
|
+
"confirm",
|
|
733
|
+
]
|
|
734
|
+
# Validate that all fields exist in Kline
|
|
735
|
+
for item in self._fields:
|
|
736
|
+
if not hasattr(Kline, item) and item != "timestamp":
|
|
737
|
+
raise ValueError(f"Field {item} does not exist in Kline")
|
|
738
|
+
|
|
739
|
+
@property
|
|
740
|
+
def df(self):
|
|
741
|
+
data = {}
|
|
742
|
+
for item in self._fields:
|
|
743
|
+
if item == "timestamp":
|
|
744
|
+
data[item] = [kline.start for kline in self]
|
|
745
|
+
else:
|
|
746
|
+
data[item] = [getattr(kline, item) for kline in self]
|
|
747
|
+
|
|
748
|
+
df = pd.DataFrame(data)
|
|
749
|
+
df["date"] = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
|
750
|
+
df.set_index("date", inplace=True)
|
|
751
|
+
return df
|
|
752
|
+
|
|
753
|
+
@property
|
|
754
|
+
def values(self):
|
|
755
|
+
return sorted(self, key=lambda x: x.timestamp)
|