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
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)