walrasquant-lib 0.5.0__tar.gz → 0.5.2__tar.gz

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 (116) hide show
  1. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/PKG-INFO +2 -1
  2. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/pyproject.toml +2 -1
  3. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/base/oms.py +1 -1
  4. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/bitget/connector.py +288 -24
  5. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/bitget/constants.py +47 -8
  6. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/bitget/oms.py +766 -5
  7. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/bitget/rest_api.py +152 -3
  8. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/bitget/schema.py +56 -0
  9. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/bitget/websockets.py +16 -2
  10. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/README.md +0 -0
  11. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/__init__.py +0 -0
  12. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/aggregation.py +0 -0
  13. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/backends/__init__.py +0 -0
  14. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/backends/db.py +0 -0
  15. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/backends/db_memory.py +0 -0
  16. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/backends/db_postgresql.py +0 -0
  17. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/backends/db_sqlite.py +0 -0
  18. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/base/__init__.py +0 -0
  19. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/base/api_client.py +0 -0
  20. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/base/connector.py +0 -0
  21. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/base/ems.py +0 -0
  22. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/base/exchange.py +0 -0
  23. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/base/retry.py +0 -0
  24. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/base/sms.py +0 -0
  25. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/base/ws_client.py +0 -0
  26. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/config.py +0 -0
  27. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/constants.py +0 -0
  28. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/core/__init__.py +0 -0
  29. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/core/cache.py +0 -0
  30. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/core/clock.py +0 -0
  31. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/core/connection.py +0 -0
  32. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/core/entity.py +0 -0
  33. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/core/nautilius_core.py +0 -0
  34. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/core/registry.py +0 -0
  35. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/engine.py +0 -0
  36. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/error.py +0 -0
  37. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/__init__.py +0 -0
  38. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/base_factory.py +0 -0
  39. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/binance/__init__.py +0 -0
  40. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/binance/connector.py +0 -0
  41. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/binance/constants.py +0 -0
  42. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/binance/ems.py +0 -0
  43. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/binance/error.py +0 -0
  44. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/binance/exchange.py +0 -0
  45. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/binance/factory.py +0 -0
  46. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/binance/oms.py +0 -0
  47. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/binance/rest_api.py +0 -0
  48. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/binance/schema.py +0 -0
  49. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/binance/websockets.py +0 -0
  50. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/bitget/__init__.py +0 -0
  51. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/bitget/ems.py +0 -0
  52. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/bitget/error.py +0 -0
  53. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/bitget/exchange.py +0 -0
  54. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/bitget/factory.py +0 -0
  55. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/bybit/__init__.py +0 -0
  56. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/bybit/connector.py +0 -0
  57. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/bybit/constants.py +0 -0
  58. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/bybit/ems.py +0 -0
  59. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/bybit/error.py +0 -0
  60. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/bybit/exchange.py +0 -0
  61. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/bybit/factory.py +0 -0
  62. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/bybit/oms.py +0 -0
  63. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/bybit/rest_api.py +0 -0
  64. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/bybit/schema.py +0 -0
  65. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/bybit/websockets.py +0 -0
  66. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/deribit/__init__.py +0 -0
  67. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/deribit/connector.py +0 -0
  68. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/deribit/constants.py +0 -0
  69. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/deribit/ems.py +0 -0
  70. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/deribit/error.py +0 -0
  71. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/deribit/exchange.py +0 -0
  72. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/deribit/factory.py +0 -0
  73. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/deribit/oms.py +0 -0
  74. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/deribit/rest_api.py +0 -0
  75. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/deribit/schema.py +0 -0
  76. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/deribit/websockets.py +0 -0
  77. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/hyperliquid/__init__.py +0 -0
  78. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/hyperliquid/connector.py +0 -0
  79. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/hyperliquid/constants.py +0 -0
  80. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/hyperliquid/ems.py +0 -0
  81. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/hyperliquid/error.py +0 -0
  82. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/hyperliquid/exchange.py +0 -0
  83. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/hyperliquid/factory.py +0 -0
  84. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/hyperliquid/oms.py +0 -0
  85. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/hyperliquid/rest_api.py +0 -0
  86. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/hyperliquid/schema.py +0 -0
  87. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/hyperliquid/signing.py +0 -0
  88. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/hyperliquid/websockets.py +0 -0
  89. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/okx/__init__.py +0 -0
  90. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/okx/connector.py +0 -0
  91. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/okx/constants.py +0 -0
  92. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/okx/ems.py +0 -0
  93. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/okx/error.py +0 -0
  94. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/okx/exchange.py +0 -0
  95. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/okx/factory.py +0 -0
  96. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/okx/oms.py +0 -0
  97. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/okx/rest_api.py +0 -0
  98. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/okx/schema.py +0 -0
  99. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/okx/websockets.py +0 -0
  100. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/exchange/registry.py +0 -0
  101. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/execution/__init__.py +0 -0
  102. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/execution/algorithm.py +0 -0
  103. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/execution/algorithms/__init__.py +0 -0
  104. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/execution/algorithms/twap.py +0 -0
  105. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/execution/config.py +0 -0
  106. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/execution/constants.py +0 -0
  107. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/execution/schema.py +0 -0
  108. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/indicator.py +0 -0
  109. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/push.py +0 -0
  110. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/schema.py +0 -0
  111. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/strategy.py +0 -0
  112. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/tools/__init__.py +0 -0
  113. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/tools/pm2_wrapper.py +0 -0
  114. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/web/__init__.py +0 -0
  115. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/web/app.py +0 -0
  116. {walrasquant_lib-0.5.0 → walrasquant_lib-0.5.2}/src/walrasquant/web/server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: walrasquant-lib
3
- Version: 0.5.0
3
+ Version: 0.5.2
4
4
  Summary: fastest python trading bot
5
5
  Author: River-Shi
6
6
  Author-email: River-Shi <nachuan.shi.quant@gmail.com>
@@ -30,6 +30,7 @@ Requires-Dist: msgspec>=0.21.1
30
30
  Requires-Dist: pandas<3.0.0
31
31
  Requires-Dist: numpy>=2.2.0
32
32
  Requires-Dist: ccxt>=4.5.64
33
+ Requires-Dist: aiofastnet>=0.21.0
33
34
  Requires-Python: >=3.11, <3.15
34
35
  Description-Content-Type: text/markdown
35
36
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "walrasquant-lib"
3
- version = "0.5.0"
3
+ version = "0.5.2"
4
4
  description = "fastest python trading bot"
5
5
  authors = [
6
6
  {name = "River-Shi",email = "nachuan.shi.quant@gmail.com"}
@@ -34,6 +34,7 @@ dependencies = [
34
34
  "pandas<3.0.0",
35
35
  "numpy>=2.2.0",
36
36
  "ccxt>=4.5.64",
37
+ "aiofastnet>=0.21.0",
37
38
  ]
38
39
 
39
40
  [project.scripts]
@@ -513,7 +513,7 @@ class OrderManagementSystem(ABC):
513
513
  pass
514
514
 
515
515
  @abstractmethod
516
- async def cancel_batch_orders(self, orders: List[CancelOrderSubmit]) -> None:
516
+ async def cancel_batch_orders(self, orders: List[CancelOrderSubmit]) -> bool | None:
517
517
  """Cancel multiple orders in a single batch request"""
518
518
  pass
519
519
 
@@ -12,11 +12,11 @@ from walrasquant.schema import (
12
12
  BookL1,
13
13
  Trade,
14
14
  Kline,
15
- # BookL2,
16
- # BookOrderData,
17
- # FundingRate,
18
- # IndexPrice,
19
- # MarkPrice,
15
+ BookL2,
16
+ BookOrderData,
17
+ FundingRate,
18
+ IndexPrice,
19
+ MarkPrice,
20
20
  KlineList,
21
21
  Ticker,
22
22
  )
@@ -31,6 +31,8 @@ from walrasquant.exchange.bitget.schema import (
31
31
  BitgetBooks1WsMsg,
32
32
  BitgetWsTradeWsMsg,
33
33
  BitgetWsCandleWsMsg,
34
+ BitgetBooksWsMsg,
35
+ BitgetWsTickerWsMsg,
34
36
  )
35
37
  from walrasquant.exchange.bitget.rest_api import BitgetApiClient
36
38
  from walrasquant.exchange.bitget.websockets import BitgetWSClient
@@ -48,6 +50,7 @@ from walrasquant.exchange.bitget.exchange import BitgetExchangeManager
48
50
  class BitgetPublicConnector(PublicConnector):
49
51
  _uta_inst_type_map = {
50
52
  BitgetUtaInstType.SPOT: "spot",
53
+ BitgetUtaInstType.MARGIN: "spot",
51
54
  BitgetUtaInstType.USDC_FUTURES: "linear",
52
55
  BitgetUtaInstType.USDT_FUTURES: "linear",
53
56
  BitgetUtaInstType.COIN_FUTURES: "inverse",
@@ -109,6 +112,9 @@ class BitgetPublicConnector(PublicConnector):
109
112
  self._ws_books1_decoder = msgspec.json.Decoder(BitgetBooks1WsMsg)
110
113
  self._ws_trade_decoder = msgspec.json.Decoder(BitgetWsTradeWsMsg)
111
114
  self._ws_candle_decoder = msgspec.json.Decoder(BitgetWsCandleWsMsg)
115
+ self._ws_books_decoder = msgspec.json.Decoder(BitgetBooksWsMsg)
116
+ self._ws_ticker_decoder = msgspec.json.Decoder(BitgetWsTickerWsMsg)
117
+ self._last_kline: Dict[tuple[str, KlineInterval], Kline] = {}
112
118
 
113
119
  def _get_inst_type(self, market: BitgetMarket):
114
120
  if market.spot:
@@ -118,6 +124,15 @@ class BitgetPublicConnector(PublicConnector):
118
124
  elif market.inverse:
119
125
  return "coin-futures"
120
126
 
127
+ def _get_category(self, market: BitgetMarket) -> str:
128
+ if market.spot:
129
+ return "SPOT"
130
+ if market.linear:
131
+ return "USDT-FUTURES" if market.quote == "USDT" else "USDC-FUTURES"
132
+ if market.inverse:
133
+ return "COIN-FUTURES"
134
+ raise ValueError(f"Unsupported market type: {market.type}")
135
+
121
136
  def _uta_inst_type_suffix(self, inst_type: BitgetUtaInstType):
122
137
  return self._uta_inst_type_map[inst_type]
123
138
 
@@ -133,7 +148,14 @@ class BitgetPublicConnector(PublicConnector):
133
148
  end_time: int | None = None,
134
149
  ) -> KlineList:
135
150
  """Request klines"""
136
- raise NotImplementedError
151
+ return await self._request_klines(
152
+ symbol=symbol,
153
+ interval=interval,
154
+ kline_type="MARKET",
155
+ limit=limit,
156
+ start_time=start_time,
157
+ end_time=end_time,
158
+ )
137
159
 
138
160
  async def request_ticker(
139
161
  self,
@@ -144,16 +166,11 @@ class BitgetPublicConnector(PublicConnector):
144
166
  if not market:
145
167
  raise ValueError(f"Symbol is not available: {symbol}")
146
168
 
147
- if market.spot:
148
- category = "SPOT"
149
- elif market.linear:
150
- category = "USDT-FUTURES" if market.quote == "USDT" else "USDC-FUTURES"
151
- elif market.inverse:
152
- category = "COIN-FUTURES"
169
+ category = self._get_category(market)
153
170
 
154
171
  res = await self._api_client.get_api_v3_market_tickers(
155
172
  category=category,
156
- symbol=symbol,
173
+ symbol=market.id,
157
174
  )
158
175
 
159
176
  ticker_response = res.data[0]
@@ -174,6 +191,8 @@ class BitgetPublicConnector(PublicConnector):
174
191
  ) -> Dict[str, Ticker]:
175
192
  """Request 24hr ticker data for multiple symbols"""
176
193
  all_tickers: Dict[str, Ticker] = {}
194
+ # COIN-FUTURES (inverse) is intentionally excluded from load_markets, so
195
+ # its tickers would all be dropped by the market_id lookup below.
177
196
  for category in ["SPOT", "USDT-FUTURES", "USDC-FUTURES"]:
178
197
  res = await self._api_client.get_api_v3_market_tickers(
179
198
  category=category,
@@ -207,7 +226,118 @@ class BitgetPublicConnector(PublicConnector):
207
226
  end_time: int | None = None,
208
227
  ) -> KlineList:
209
228
  """Request index klines"""
210
- raise NotImplementedError
229
+ market = self._market.get(symbol)
230
+ if not market:
231
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
232
+ if market.spot:
233
+ raise ValueError("Spot market is not supported for index klines")
234
+ return await self._request_klines(
235
+ symbol=symbol,
236
+ interval=interval,
237
+ kline_type="INDEX",
238
+ limit=limit,
239
+ start_time=start_time,
240
+ end_time=end_time,
241
+ )
242
+
243
+ async def _request_klines(
244
+ self,
245
+ *,
246
+ symbol: str,
247
+ interval: KlineInterval,
248
+ kline_type: str,
249
+ limit: int | None,
250
+ start_time: int | None,
251
+ end_time: int | None,
252
+ ) -> KlineList:
253
+ market = self._market.get(symbol)
254
+ if not market:
255
+ raise ValueError(f"Symbol {symbol} formated wrongly, or not supported")
256
+
257
+ bitget_interval = BitgetEnumParser.to_bitget_kline_interval(interval)
258
+ all_klines: list[Kline] = []
259
+ seen_timestamps: set[int] = set()
260
+ prev_start_time: int | None = None
261
+
262
+ while True:
263
+ if prev_start_time is not None and prev_start_time == start_time:
264
+ break
265
+ prev_start_time = start_time
266
+
267
+ response = await self._api_client.get_api_v3_market_candles(
268
+ category=self._get_category(market),
269
+ symbol=market.id,
270
+ interval=bitget_interval.value,
271
+ type=kline_type,
272
+ startTime=start_time,
273
+ endTime=end_time,
274
+ limit=100,
275
+ )
276
+ response_rows = sorted(response.data, key=lambda row: int(row[0]))
277
+ if not response_rows:
278
+ break
279
+
280
+ new_klines = [
281
+ self._handle_candlesticks(
282
+ symbol=symbol,
283
+ interval=interval,
284
+ row=row,
285
+ timestamp=response.requestTime,
286
+ )
287
+ for row in response_rows
288
+ if int(row[0]) not in seen_timestamps
289
+ ]
290
+ if not new_klines:
291
+ break
292
+
293
+ all_klines.extend(new_klines)
294
+ seen_timestamps.update(int(row[0]) for row in response_rows)
295
+ start_time = int(response_rows[-1][0]) + 1
296
+ if end_time is not None and start_time >= end_time:
297
+ break
298
+
299
+ if limit is not None and len(all_klines) > limit:
300
+ all_klines = all_klines[-limit:]
301
+
302
+ return KlineList(
303
+ all_klines,
304
+ fields=[
305
+ "timestamp",
306
+ "symbol",
307
+ "open",
308
+ "high",
309
+ "low",
310
+ "close",
311
+ "volume",
312
+ "quote_volume",
313
+ "confirm",
314
+ ],
315
+ )
316
+
317
+ def _handle_candlesticks(
318
+ self,
319
+ *,
320
+ symbol: str,
321
+ interval: KlineInterval,
322
+ row: List[str],
323
+ timestamp: int,
324
+ ) -> Kline:
325
+ start = int(row[0])
326
+ confirm = self._clock.timestamp_ms() >= start + interval.seconds * 1000 - 1
327
+ return Kline(
328
+ exchange=self._exchange_id,
329
+ symbol=symbol,
330
+ interval=interval,
331
+ open=float(row[1]),
332
+ high=float(row[2]),
333
+ low=float(row[3]),
334
+ close=float(row[4]),
335
+ volume=float(row[5]),
336
+ quote_volume=float(row[6]),
337
+ start=start,
338
+ timestamp=timestamp,
339
+ confirm=confirm,
340
+ )
211
341
 
212
342
  def _ws_msg_handler(self, raw: bytes):
213
343
  """Handle incoming WebSocket messages"""
@@ -224,10 +354,14 @@ class BitgetPublicConnector(PublicConnector):
224
354
  elif msg.arg is not None:
225
355
  if msg.arg.topic == "books1":
226
356
  self._handle_books1_data(raw, msg.arg)
357
+ elif msg.arg.topic in ("books5", "books50"):
358
+ self._handle_books_data(raw, msg.arg)
227
359
  elif msg.arg.topic == "publicTrade":
228
360
  self._handle_trade_data(raw, msg.arg)
229
361
  elif msg.arg.topic == "kline":
230
362
  self._handle_candle_data(raw, msg.arg)
363
+ elif msg.arg.topic == "ticker":
364
+ self._handle_ticker_data(raw, msg.arg)
231
365
 
232
366
  # print(f"Received WebSocket message: {raw}")
233
367
  except msgspec.DecodeError as e:
@@ -251,6 +385,12 @@ class BitgetPublicConnector(PublicConnector):
251
385
  BitgetKlineInterval(arg.interval)
252
386
  )
253
387
  for data in msg.data:
388
+ key = (symbol, interval)
389
+ cached = self._last_kline.get(key)
390
+ if cached is not None and int(data.start) > cached.start:
391
+ self._msgbus.publish(
392
+ topic="kline", msg=msgspec.structs.replace(cached, confirm=True)
393
+ )
254
394
  kline = Kline(
255
395
  exchange=self._exchange_id,
256
396
  symbol=symbol,
@@ -263,10 +403,11 @@ class BitgetPublicConnector(PublicConnector):
263
403
  start=int(data.start),
264
404
  timestamp=self._clock.timestamp_ms(),
265
405
  interval=interval,
266
- confirm=False, # NOTE: need to handle confirm yourself
406
+ confirm=False,
267
407
  )
268
408
  self._msgbus.publish(topic="kline", msg=kline)
269
- # self._log.debug(f"Kline update: {str(kline)}")
409
+ if cached is None or kline.start >= cached.start:
410
+ self._last_kline[key] = kline
270
411
 
271
412
  def _handle_trade_data(self, raw: bytes, arg: BitgetWsUtaArgMsg):
272
413
  msg = self._ws_trade_decoder.decode(raw)
@@ -302,6 +443,68 @@ class BitgetPublicConnector(PublicConnector):
302
443
  self._msgbus.publish(topic="bookl1", msg=bookl1)
303
444
  # self._log.debug(f"BookL1 update: {str(bookl1)}")
304
445
 
446
+ def _handle_books_data(self, raw: bytes, arg: BitgetWsUtaArgMsg):
447
+ msg = self._ws_books_decoder.decode(raw)
448
+ sym_id = f"{arg.symbol}_{self._uta_inst_type_suffix(arg.instType)}"
449
+ symbol = self._market_id[sym_id]
450
+ for data in msg.data:
451
+ bids = [
452
+ BookOrderData(price=float(book.px), size=float(book.sz))
453
+ for book in data.b
454
+ ] or [BookOrderData(price=0, size=0)]
455
+ asks = [
456
+ BookOrderData(price=float(book.px), size=float(book.sz))
457
+ for book in data.a
458
+ ] or [BookOrderData(price=0, size=0)]
459
+ self._msgbus.publish(
460
+ topic="bookl2",
461
+ msg=BookL2(
462
+ exchange=self._exchange_id,
463
+ symbol=symbol,
464
+ bids=bids,
465
+ asks=asks,
466
+ timestamp=int(data.ts),
467
+ ),
468
+ )
469
+
470
+ def _handle_ticker_data(self, raw: bytes, arg: BitgetWsUtaArgMsg):
471
+ msg = self._ws_ticker_decoder.decode(raw)
472
+ sym_id = f"{arg.symbol}_{self._uta_inst_type_suffix(arg.instType)}"
473
+ symbol = self._market_id[sym_id]
474
+ timestamp = int(msg.ts) if msg.ts is not None else self._clock.timestamp_ms()
475
+ for data in msg.data:
476
+ if data.fundingRate is not None and data.nextFundingTime is not None:
477
+ self._msgbus.publish(
478
+ topic="funding_rate",
479
+ msg=FundingRate(
480
+ exchange=self._exchange_id,
481
+ symbol=symbol,
482
+ rate=float(data.fundingRate),
483
+ next_funding_time=int(data.nextFundingTime),
484
+ timestamp=timestamp,
485
+ ),
486
+ )
487
+ if data.indexPrice is not None:
488
+ self._msgbus.publish(
489
+ topic="index_price",
490
+ msg=IndexPrice(
491
+ exchange=self._exchange_id,
492
+ symbol=symbol,
493
+ price=float(data.indexPrice),
494
+ timestamp=timestamp,
495
+ ),
496
+ )
497
+ if data.markPrice is not None:
498
+ self._msgbus.publish(
499
+ topic="mark_price",
500
+ msg=MarkPrice(
501
+ exchange=self._exchange_id,
502
+ symbol=symbol,
503
+ price=float(data.markPrice),
504
+ timestamp=timestamp,
505
+ ),
506
+ )
507
+
305
508
  def subscribe_bookl1(self, symbol: str | List[str]):
306
509
  symbol = symbol if isinstance(symbol, list) else [symbol]
307
510
  symbols_by_inst_type = {}
@@ -408,35 +611,92 @@ class BitgetPublicConnector(PublicConnector):
408
611
 
409
612
  def subscribe_bookl2(self, symbol: str | List[str], level: BookLevel):
410
613
  """Subscribe to the bookl2 data"""
411
- raise NotImplementedError
614
+ topic_by_level = {BookLevel.L5: "books5", BookLevel.L50: "books50"}
615
+ topic = topic_by_level.get(level)
616
+ if topic is None:
617
+ raise ValueError(
618
+ f"Unsupported book level for Bitget: {level}. Use L5 or L50."
619
+ )
620
+ symbol = symbol if isinstance(symbol, list) else [symbol]
621
+ symbols_by_inst_type: Dict[str, List[str]] = {}
622
+ for sym in symbol:
623
+ market = self._market.get(sym)
624
+ if not market:
625
+ raise ValueError(f"Symbol {sym} not found in market data.")
626
+ symbols_by_inst_type.setdefault(self._get_inst_type(market), []).append(
627
+ market.id
628
+ )
629
+ for inst_type, symbols in symbols_by_inst_type.items():
630
+ self._ws_client.subscribe_depth_v3(symbols, inst_type, topic)
412
631
 
413
632
  def unsubscribe_bookl2(self, symbol: str | List[str], level: BookLevel):
414
633
  """Unsubscribe from the bookl2 data"""
415
- raise NotImplementedError
634
+ topic_by_level = {BookLevel.L5: "books5", BookLevel.L50: "books50"}
635
+ topic = topic_by_level.get(level)
636
+ if topic is None:
637
+ raise ValueError(
638
+ f"Unsupported book level for Bitget: {level}. Use L5 or L50."
639
+ )
640
+ symbol = symbol if isinstance(symbol, list) else [symbol]
641
+ symbols_by_inst_type: Dict[str, List[str]] = {}
642
+ for sym in symbol:
643
+ market = self._market.get(sym)
644
+ if not market:
645
+ raise ValueError(f"Symbol {sym} not found in market data.")
646
+ symbols_by_inst_type.setdefault(self._get_inst_type(market), []).append(
647
+ market.id
648
+ )
649
+ for inst_type, symbols in symbols_by_inst_type.items():
650
+ self._ws_client.unsubscribe_depth_v3(symbols, inst_type, topic)
651
+
652
+ def _ticker_symbols_by_inst_type(
653
+ self, symbol: str | List[str]
654
+ ) -> Dict[str, List[str]]:
655
+ symbol = symbol if isinstance(symbol, list) else [symbol]
656
+ symbols_by_inst_type: Dict[str, List[str]] = {}
657
+ for sym in symbol:
658
+ market = self._market.get(sym)
659
+ if not market:
660
+ raise ValueError(f"Symbol {sym} not found in market data.")
661
+ if market.spot:
662
+ raise ValueError(
663
+ "Funding, index, and mark ticker subscriptions are not "
664
+ "supported for Bitget spot markets"
665
+ )
666
+ symbols_by_inst_type.setdefault(self._get_inst_type(market), []).append(
667
+ market.id
668
+ )
669
+ return symbols_by_inst_type
416
670
 
417
671
  def subscribe_funding_rate(self, symbol: str | List[str]):
418
672
  """Subscribe to the funding rate data"""
419
- raise NotImplementedError
673
+ for inst_type, symbols in self._ticker_symbols_by_inst_type(symbol).items():
674
+ self._ws_client.subscribe_ticker_v3(symbols, inst_type)
420
675
 
421
676
  def unsubscribe_funding_rate(self, symbol: str | List[str]):
422
677
  """Unsubscribe from the funding rate data"""
423
- raise NotImplementedError
678
+ for inst_type, symbols in self._ticker_symbols_by_inst_type(symbol).items():
679
+ self._ws_client.unsubscribe_ticker_v3(symbols, inst_type)
424
680
 
425
681
  def subscribe_index_price(self, symbol: str | List[str]):
426
682
  """Subscribe to the index price data"""
427
- raise NotImplementedError
683
+ for inst_type, symbols in self._ticker_symbols_by_inst_type(symbol).items():
684
+ self._ws_client.subscribe_ticker_v3(symbols, inst_type)
428
685
 
429
686
  def unsubscribe_index_price(self, symbol: str | List[str]):
430
687
  """Unsubscribe from the index price data"""
431
- raise NotImplementedError
688
+ for inst_type, symbols in self._ticker_symbols_by_inst_type(symbol).items():
689
+ self._ws_client.unsubscribe_ticker_v3(symbols, inst_type)
432
690
 
433
691
  def subscribe_mark_price(self, symbol: str | List[str]):
434
692
  """Subscribe to the mark price data"""
435
- raise NotImplementedError
693
+ for inst_type, symbols in self._ticker_symbols_by_inst_type(symbol).items():
694
+ self._ws_client.subscribe_ticker_v3(symbols, inst_type)
436
695
 
437
696
  def unsubscribe_mark_price(self, symbol: str | List[str]):
438
697
  """Unsubscribe from the mark price data"""
439
- raise NotImplementedError
698
+ for inst_type, symbols in self._ticker_symbols_by_inst_type(symbol).items():
699
+ self._ws_client.unsubscribe_ticker_v3(symbols, inst_type)
440
700
 
441
701
 
442
702
  class BitgetPrivateConnector(PrivateConnector):
@@ -508,6 +768,10 @@ class BitgetPrivateConnector(PrivateConnector):
508
768
 
509
769
  async def connect(self):
510
770
  if self._account_type.is_uta:
771
+ self._log.warning(
772
+ "Bitget UTA private trading support is beta and under development; "
773
+ "use classic SPOT/FUTURE accounts for stable trading workflows."
774
+ )
511
775
  self._oms._ws_client.subscribe_v3_order()
512
776
  self._oms._ws_client.subscribe_v3_position()
513
777
  self._oms._ws_client.subscribe_v3_account()
@@ -10,6 +10,7 @@ from walrasquant.constants import (
10
10
  OrderType,
11
11
  KlineInterval,
12
12
  RateLimiter,
13
+ TriggerType,
13
14
  )
14
15
  from walrasquant.error import KlineSupportedError
15
16
  from walrasquant.exchange.bitget.error import BitgetRateLimitError
@@ -95,6 +96,7 @@ class BitgetInstType(Enum):
95
96
 
96
97
  class BitgetUtaInstType(Enum):
97
98
  SPOT = "spot"
99
+ MARGIN = "margin"
98
100
  USDT_FUTURES = "usdt-futures"
99
101
  COIN_FUTURES = "coin-futures"
100
102
  USDC_FUTURES = "usdc-futures"
@@ -102,31 +104,39 @@ class BitgetUtaInstType(Enum):
102
104
 
103
105
  @property
104
106
  def is_spot(self) -> bool:
105
- return self == BitgetInstType.SPOT
107
+ return self == BitgetUtaInstType.SPOT
106
108
 
107
109
  @property
108
110
  def is_swap(self) -> bool:
109
- return not self.is_spot
111
+ return self in (
112
+ BitgetUtaInstType.USDT_FUTURES,
113
+ BitgetUtaInstType.USDC_FUTURES,
114
+ BitgetUtaInstType.COIN_FUTURES,
115
+ )
110
116
 
111
117
  @property
112
118
  def is_linear(self) -> bool:
113
- return self in (BitgetInstType.USDT_FUTURES, BitgetInstType.USDC_FUTURES)
119
+ return self in (
120
+ BitgetUtaInstType.USDT_FUTURES,
121
+ BitgetUtaInstType.USDC_FUTURES,
122
+ )
114
123
 
115
124
  @property
116
125
  def is_inverse(self) -> bool:
117
- return self == BitgetInstType.COIN_FUTURES
126
+ return self == BitgetUtaInstType.COIN_FUTURES
118
127
 
119
128
  @property
120
129
  def is_usdc_swap(self) -> bool:
121
- return self == BitgetInstType.USDC_FUTURES
130
+ return self == BitgetUtaInstType.USDC_FUTURES
122
131
 
123
132
  @property
124
133
  def is_usdt_swap(self) -> bool:
125
- return self == BitgetInstType.USDT_FUTURES
134
+ return self == BitgetUtaInstType.USDT_FUTURES
126
135
 
127
136
 
128
137
  class BitgetKlineInterval(Enum):
129
138
  MINUTE_1 = "1m"
139
+ MINUTE_3 = "3m"
130
140
  MINUTE_5 = "5m"
131
141
  MINUTE_15 = "15m"
132
142
  MINUTE_30 = "30m"
@@ -147,7 +157,7 @@ class BitgetOrderStatus(Enum):
147
157
  NEW = "new"
148
158
  FILLED = "filled"
149
159
  CANCELED = "canceled"
150
- CANCELLED = "cancelled" # Alias pointing to the same value
160
+ CANCELLED = "cancelled" # Used by endpoints that spell canceled with two l's
151
161
  PARTIALLY_FILLED = "partially_filled"
152
162
 
153
163
 
@@ -216,6 +226,7 @@ class BitgetPositionSide(Enum):
216
226
  class BitgetEnumParser:
217
227
  _kline_interval_map = {
218
228
  BitgetKlineInterval.MINUTE_1: KlineInterval.MINUTE_1,
229
+ BitgetKlineInterval.MINUTE_3: KlineInterval.MINUTE_3,
219
230
  BitgetKlineInterval.MINUTE_5: KlineInterval.MINUTE_5,
220
231
  BitgetKlineInterval.MINUTE_15: KlineInterval.MINUTE_15,
221
232
  BitgetKlineInterval.MINUTE_30: KlineInterval.MINUTE_30,
@@ -231,7 +242,7 @@ class BitgetEnumParser:
231
242
  BitgetOrderStatus.NEW: OrderStatus.ACCEPTED,
232
243
  BitgetOrderStatus.FILLED: OrderStatus.FILLED,
233
244
  BitgetOrderStatus.CANCELED: OrderStatus.CANCELED,
234
- BitgetOrderStatus.CANCELLED: OrderStatus.CANCELED, # Alias pointing to the same value
245
+ BitgetOrderStatus.CANCELLED: OrderStatus.CANCELED,
235
246
  BitgetOrderStatus.PARTIALLY_FILLED: OrderStatus.PARTIALLY_FILLED,
236
247
  }
237
248
 
@@ -297,6 +308,14 @@ class BitgetEnumParser:
297
308
  def parse_order_type(cls, order_type: BitgetOrderType) -> OrderType:
298
309
  return cls._order_type_map[order_type]
299
310
 
311
+ @classmethod
312
+ def to_bitget_trigger_type(cls, trigger_type: TriggerType) -> str:
313
+ if trigger_type == TriggerType.LAST_PRICE:
314
+ return "market"
315
+ if trigger_type == TriggerType.MARK_PRICE:
316
+ return "mark"
317
+ raise ValueError(f"Unsupported Bitget trigger type: {trigger_type}")
318
+
300
319
 
301
320
  class BitgetRateLimiter(RateLimiter):
302
321
  def __init__(self, enable_rate_limit: bool = True):
@@ -332,6 +351,26 @@ class BitgetRateLimiter(RateLimiter):
332
351
  timeout=-1,
333
352
  using=RateLimiterType.FIXED_WINDOW.value,
334
353
  ),
354
+ "/api/v3/trade/place-batch": Throttled(
355
+ quota=rate_limiter.per_sec(5),
356
+ timeout=-1,
357
+ using=RateLimiterType.FIXED_WINDOW.value,
358
+ ),
359
+ "/api/v3/trade/modify-order": Throttled(
360
+ quota=rate_limiter.per_sec(10),
361
+ timeout=-1,
362
+ using=RateLimiterType.FIXED_WINDOW.value,
363
+ ),
364
+ "/api/v2/spot/trade/batch-orders": Throttled(
365
+ quota=rate_limiter.per_sec(5),
366
+ timeout=-1,
367
+ using=RateLimiterType.FIXED_WINDOW.value,
368
+ ),
369
+ "/api/v2/mix/order/batch-place-order": Throttled(
370
+ quota=rate_limiter.per_sec(5),
371
+ timeout=-1,
372
+ using=RateLimiterType.FIXED_WINDOW.value,
373
+ ),
335
374
  "/api/v3/trade/cancel-order": Throttled(
336
375
  quota=rate_limiter.per_sec(10),
337
376
  timeout=-1,