walrasquant-lib 0.4.27__tar.gz → 0.4.28__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 (115) hide show
  1. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/PKG-INFO +1 -1
  2. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/pyproject.toml +1 -1
  3. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/base/ems.py +36 -47
  4. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/constants.py +1 -0
  5. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/__init__.py +2 -0
  6. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/bitget/ems.py +6 -37
  7. walrasquant_lib-0.4.28/src/walrasquant/exchange/deribit/__init__.py +26 -0
  8. walrasquant_lib-0.4.28/src/walrasquant/exchange/deribit/connector.py +439 -0
  9. walrasquant_lib-0.4.28/src/walrasquant/exchange/deribit/constants.py +112 -0
  10. walrasquant_lib-0.4.28/src/walrasquant/exchange/deribit/ems.py +66 -0
  11. walrasquant_lib-0.4.28/src/walrasquant/exchange/deribit/exchange.py +86 -0
  12. walrasquant_lib-0.4.28/src/walrasquant/exchange/deribit/factory.py +101 -0
  13. walrasquant_lib-0.4.28/src/walrasquant/exchange/deribit/oms.py +670 -0
  14. walrasquant_lib-0.4.28/src/walrasquant/exchange/deribit/rest_api.py +106 -0
  15. walrasquant_lib-0.4.28/src/walrasquant/exchange/deribit/schema.py +41 -0
  16. walrasquant_lib-0.4.28/src/walrasquant/exchange/deribit/websockets.py +184 -0
  17. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/README.md +0 -0
  18. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/__init__.py +0 -0
  19. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/aggregation.py +0 -0
  20. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/backends/__init__.py +0 -0
  21. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/backends/db.py +0 -0
  22. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/backends/db_memory.py +0 -0
  23. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/backends/db_postgresql.py +0 -0
  24. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/backends/db_sqlite.py +0 -0
  25. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/base/__init__.py +0 -0
  26. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/base/api_client.py +0 -0
  27. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/base/connector.py +0 -0
  28. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/base/exchange.py +0 -0
  29. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/base/oms.py +0 -0
  30. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/base/retry.py +0 -0
  31. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/base/sms.py +0 -0
  32. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/base/ws_client.py +0 -0
  33. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/config.py +0 -0
  34. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/core/__init__.py +0 -0
  35. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/core/cache.py +0 -0
  36. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/core/clock.py +0 -0
  37. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/core/connection.py +0 -0
  38. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/core/entity.py +0 -0
  39. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/core/nautilius_core.py +0 -0
  40. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/core/registry.py +0 -0
  41. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/engine.py +0 -0
  42. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/error.py +0 -0
  43. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/base_factory.py +0 -0
  44. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/binance/__init__.py +0 -0
  45. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/binance/connector.py +0 -0
  46. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/binance/constants.py +0 -0
  47. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/binance/ems.py +0 -0
  48. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/binance/error.py +0 -0
  49. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/binance/exchange.py +0 -0
  50. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/binance/factory.py +0 -0
  51. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/binance/oms.py +0 -0
  52. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/binance/rest_api.py +0 -0
  53. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/binance/schema.py +0 -0
  54. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/binance/websockets.py +0 -0
  55. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/bitget/__init__.py +0 -0
  56. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/bitget/connector.py +0 -0
  57. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/bitget/constants.py +0 -0
  58. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/bitget/error.py +0 -0
  59. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/bitget/exchange.py +0 -0
  60. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/bitget/factory.py +0 -0
  61. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/bitget/oms.py +0 -0
  62. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/bitget/rest_api.py +0 -0
  63. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/bitget/schema.py +0 -0
  64. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/bitget/websockets.py +0 -0
  65. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/bybit/__init__.py +0 -0
  66. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/bybit/connector.py +0 -0
  67. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/bybit/constants.py +0 -0
  68. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/bybit/ems.py +0 -0
  69. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/bybit/error.py +0 -0
  70. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/bybit/exchange.py +0 -0
  71. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/bybit/factory.py +0 -0
  72. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/bybit/oms.py +0 -0
  73. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/bybit/rest_api.py +0 -0
  74. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/bybit/schema.py +0 -0
  75. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/bybit/websockets.py +0 -0
  76. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/hyperliquid/__init__.py +0 -0
  77. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/hyperliquid/connector.py +0 -0
  78. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/hyperliquid/constants.py +0 -0
  79. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/hyperliquid/ems.py +0 -0
  80. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/hyperliquid/error.py +0 -0
  81. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/hyperliquid/exchange.py +0 -0
  82. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/hyperliquid/factory.py +0 -0
  83. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/hyperliquid/oms.py +0 -0
  84. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/hyperliquid/rest_api.py +0 -0
  85. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/hyperliquid/schema.py +0 -0
  86. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/hyperliquid/signing.py +0 -0
  87. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/hyperliquid/websockets.py +0 -0
  88. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/okx/__init__.py +0 -0
  89. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/okx/connector.py +0 -0
  90. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/okx/constants.py +0 -0
  91. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/okx/ems.py +0 -0
  92. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/okx/error.py +0 -0
  93. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/okx/exchange.py +0 -0
  94. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/okx/factory.py +0 -0
  95. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/okx/oms.py +0 -0
  96. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/okx/rest_api.py +0 -0
  97. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/okx/schema.py +0 -0
  98. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/okx/websockets.py +0 -0
  99. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/exchange/registry.py +0 -0
  100. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/execution/__init__.py +0 -0
  101. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/execution/algorithm.py +0 -0
  102. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/execution/algorithms/__init__.py +0 -0
  103. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/execution/algorithms/twap.py +0 -0
  104. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/execution/config.py +0 -0
  105. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/execution/constants.py +0 -0
  106. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/execution/schema.py +0 -0
  107. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/indicator.py +0 -0
  108. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/push.py +0 -0
  109. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/schema.py +0 -0
  110. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/strategy.py +0 -0
  111. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/tools/__init__.py +0 -0
  112. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/tools/pm2_wrapper.py +0 -0
  113. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/web/__init__.py +0 -0
  114. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/src/walrasquant/web/app.py +0 -0
  115. {walrasquant_lib-0.4.27 → walrasquant_lib-0.4.28}/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.4.27
3
+ Version: 0.4.28
4
4
  Summary: fastest python trading bot
5
5
  Author: River-Shi
6
6
  Author-email: River-Shi <nachuan.shi.quant@gmail.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "walrasquant-lib"
3
- version = "0.4.27"
3
+ version = "0.4.28"
4
4
  description = "fastest python trading bot"
5
5
  authors = [
6
6
  {name = "River-Shi",email = "nachuan.shi.quant@gmail.com"}
@@ -168,6 +168,40 @@ class ExecutionManagementSystem(ABC):
168
168
  self._build_order_submit_queues()
169
169
  self._set_account_type()
170
170
 
171
+ @staticmethod
172
+ def _quantize_step(
173
+ value: float,
174
+ step: float | str,
175
+ mode: Literal["round", "ceil", "floor"] = "round",
176
+ ) -> Decimal:
177
+ """
178
+ Snap ``value`` to the nearest integer multiple of ``step``.
179
+
180
+ Unlike ``Decimal.quantize`` (which aligns to a number of decimal
181
+ places), this aligns to the actual step value, so it is correct for
182
+ arbitrary steps such as tick sizes of 0.5 or 0.2 as well as the usual
183
+ powers of ten. The result is normalized to the number of decimal places
184
+ implied by ``step`` so its string form matches decimal-place rounding
185
+ for power-of-ten / integer steps (no spurious trailing zeros).
186
+
187
+ ``step`` arrives either as a float (ccxt ``market.precision``) or a str
188
+ (exchange-native tick fields); both are normalized via ``Decimal(str())``
189
+ to avoid binary-float error.
190
+ """
191
+ rounding = {
192
+ "round": ROUND_HALF_UP,
193
+ "ceil": ROUND_CEILING,
194
+ "floor": ROUND_FLOOR,
195
+ }[mode]
196
+ value_decimal = Decimal(str(value))
197
+ step_decimal = Decimal(str(step))
198
+ multiples = (value_decimal / step_decimal).quantize(
199
+ Decimal("1"), rounding=rounding
200
+ )
201
+ result = multiples * step_decimal
202
+ ndigits = max(0, -step_decimal.normalize().as_tuple().exponent)
203
+ return result.quantize(Decimal(1).scaleb(-ndigits))
204
+
171
205
  def _amount_to_precision(
172
206
  self,
173
207
  symbol: str,
@@ -178,29 +212,7 @@ class ExecutionManagementSystem(ABC):
178
212
  Convert the amount to the precision of the market
179
213
  """
180
214
  market = self._market[symbol]
181
- amount_decimal: Decimal = Decimal(str(amount))
182
- precision = market.precision.amount
183
-
184
- if precision >= 1:
185
- exp = Decimal(int(precision))
186
- precision_decimal = Decimal("1")
187
- else:
188
- exp = Decimal("1")
189
- precision_decimal = Decimal(str(precision))
190
-
191
- if mode == "round":
192
- format_amount = (amount_decimal / exp).quantize(
193
- precision_decimal, rounding=ROUND_HALF_UP
194
- ) * exp
195
- elif mode == "ceil":
196
- format_amount = (amount_decimal / exp).quantize(
197
- precision_decimal, rounding=ROUND_CEILING
198
- ) * exp
199
- elif mode == "floor":
200
- format_amount = (amount_decimal / exp).quantize(
201
- precision_decimal, rounding=ROUND_FLOOR
202
- ) * exp
203
- return format_amount
215
+ return self._quantize_step(amount, market.precision.amount, mode)
204
216
 
205
217
  def _price_to_precision(
206
218
  self,
@@ -212,30 +224,7 @@ class ExecutionManagementSystem(ABC):
212
224
  Convert the price to the precision of the market
213
225
  """
214
226
  market = self._market[symbol]
215
- price_decimal: Decimal = Decimal(str(price))
216
-
217
- decimal = market.precision.price
218
-
219
- if decimal >= 1:
220
- exp = Decimal(int(decimal))
221
- precision_decimal = Decimal("1")
222
- else:
223
- exp = Decimal("1")
224
- precision_decimal = Decimal(str(decimal))
225
-
226
- if mode == "round":
227
- format_price = (price_decimal / exp).quantize(
228
- precision_decimal, rounding=ROUND_HALF_UP
229
- ) * exp
230
- elif mode == "ceil":
231
- format_price = (price_decimal / exp).quantize(
232
- precision_decimal, rounding=ROUND_CEILING
233
- ) * exp
234
- elif mode == "floor":
235
- format_price = (price_decimal / exp).quantize(
236
- precision_decimal, rounding=ROUND_FLOOR
237
- ) * exp
238
- return format_price
227
+ return self._quantize_step(price, market.precision.price, mode)
239
228
 
240
229
  @abstractmethod
241
230
  def _instrument_id_to_account_type(
@@ -225,6 +225,7 @@ class ExchangeType(Enum):
225
225
  BYBIT = "bybit"
226
226
  HYPERLIQUID = "hyperliquid"
227
227
  BITGET = "bitget"
228
+ DERIBIT = "deribit"
228
229
 
229
230
 
230
231
  class AccountType(Enum):
@@ -3,6 +3,7 @@ from walrasquant.exchange.bybit import BybitAccountType
3
3
  from walrasquant.exchange.okx import OkxAccountType
4
4
  from walrasquant.exchange.hyperliquid import HyperLiquidAccountType
5
5
  from walrasquant.exchange.bitget import BitgetAccountType
6
+ from walrasquant.exchange.deribit import DeribitAccountType
6
7
 
7
8
  __all__ = [
8
9
  "BinanceAccountType",
@@ -10,4 +11,5 @@ __all__ = [
10
11
  "OkxAccountType",
11
12
  "HyperLiquidAccountType",
12
13
  "BitgetAccountType",
14
+ "DeribitAccountType",
13
15
  ]
@@ -1,7 +1,6 @@
1
1
  from typing import Dict, cast
2
2
  from decimal import Decimal
3
3
  from typing import Literal
4
- from decimal import ROUND_HALF_UP, ROUND_CEILING, ROUND_FLOOR
5
4
 
6
5
  from walrasquant.constants import AccountType
7
6
  from walrasquant.schema import (
@@ -124,26 +123,11 @@ class BitgetExecutionManagementSystem(ExecutionManagementSystem):
124
123
  mode: Literal["round", "ceil", "floor"] = "round",
125
124
  ) -> Decimal:
126
125
  market = self._market[symbol]
126
+ # Spot amount precision is a plain ccxt step handled by the base class;
127
+ # derivatives quantize to the contract sizeMultiplier.
127
128
  if market.spot:
128
129
  return super()._amount_to_precision(symbol, amount, mode)
129
- else:
130
- amount: Decimal = Decimal(str(amount))
131
- amount_multiplier = Decimal(market.info.sizeMultiplier or "1")
132
- multiplier_count = amount / amount_multiplier
133
-
134
- if mode == "round":
135
- amount = (
136
- multiplier_count.quantize(Decimal("1"), rounding=ROUND_HALF_UP)
137
- ) * amount_multiplier
138
- elif mode == "ceil":
139
- amount = (
140
- multiplier_count.quantize(Decimal("1"), rounding=ROUND_CEILING)
141
- ) * amount_multiplier
142
- elif mode == "floor":
143
- amount = (
144
- multiplier_count.quantize(Decimal("1"), rounding=ROUND_FLOOR)
145
- ) * amount_multiplier
146
- return amount
130
+ return self._quantize_step(amount, market.info.sizeMultiplier or "1", mode)
147
131
 
148
132
  def _price_to_precision(
149
133
  self,
@@ -152,26 +136,11 @@ class BitgetExecutionManagementSystem(ExecutionManagementSystem):
152
136
  mode: Literal["round", "ceil", "floor"] = "round",
153
137
  ) -> Decimal:
154
138
  market = self._market[symbol]
139
+ # Spot price precision is a plain ccxt step handled by the base class;
140
+ # derivatives quantize to the priceEndStep tick.
155
141
  if market.spot:
156
142
  return super()._price_to_precision(symbol, price, mode)
157
- else:
158
- price: Decimal = Decimal(str(price))
159
- price_multiplier = Decimal(market.info.priceEndStep or "1")
160
- multiplier_count = price / price_multiplier
161
-
162
- if mode == "round":
163
- price = (
164
- multiplier_count.quantize(Decimal("1"), rounding=ROUND_HALF_UP)
165
- ) * price_multiplier
166
- elif mode == "ceil":
167
- price = (
168
- multiplier_count.quantize(Decimal("1"), rounding=ROUND_CEILING)
169
- ) * price_multiplier
170
- elif mode == "floor":
171
- price = (
172
- multiplier_count.quantize(Decimal("1"), rounding=ROUND_FLOOR)
173
- ) * price_multiplier
174
- return price
143
+ return self._quantize_step(price, market.info.priceEndStep or "1", mode)
175
144
 
176
145
  async def _cancel_all_orders(
177
146
  self, order_submit: CancelAllOrderSubmit, account_type: AccountType
@@ -0,0 +1,26 @@
1
+ from walrasquant.exchange.deribit.constants import DeribitAccountType
2
+ from walrasquant.exchange.deribit.connector import (
3
+ DeribitPrivateConnector,
4
+ DeribitPublicConnector,
5
+ )
6
+ from walrasquant.exchange.deribit.ems import DeribitExecutionManagementSystem
7
+ from walrasquant.exchange.deribit.exchange import DeribitExchangeManager
8
+ from walrasquant.exchange.deribit.factory import DeribitFactory
9
+ from walrasquant.exchange.deribit.oms import DeribitOrderManagementSystem
10
+
11
+ try:
12
+ from walrasquant.exchange.registry import register_factory
13
+
14
+ register_factory(DeribitFactory())
15
+ except ImportError:
16
+ pass
17
+
18
+ __all__ = [
19
+ "DeribitAccountType",
20
+ "DeribitExchangeManager",
21
+ "DeribitPublicConnector",
22
+ "DeribitPrivateConnector",
23
+ "DeribitOrderManagementSystem",
24
+ "DeribitExecutionManagementSystem",
25
+ "DeribitFactory",
26
+ ]
@@ -0,0 +1,439 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, cast
4
+
5
+ import msgspec
6
+
7
+ from walrasquant.base import PrivateConnector, PublicConnector
8
+ from walrasquant.config import OrderQueryConfig
9
+ from walrasquant.constants import BookLevel, KlineInterval
10
+ from walrasquant.core.cache import AsyncCache
11
+ from walrasquant.core.entity import TaskManager
12
+ from walrasquant.core.nautilius_core import LiveClock, MessageBus
13
+ from walrasquant.core.registry import OrderRegistry
14
+ from walrasquant.exchange.deribit.constants import (
15
+ DeribitAccountType,
16
+ DeribitEnumParser,
17
+ )
18
+ from walrasquant.exchange.deribit.exchange import DeribitExchangeManager
19
+ from walrasquant.exchange.deribit.oms import DeribitOrderManagementSystem
20
+ from walrasquant.exchange.deribit.rest_api import DeribitApiClient
21
+ from walrasquant.exchange.deribit.websockets import DeribitWSClient
22
+ from walrasquant.schema import (
23
+ BookL1,
24
+ BookL2,
25
+ BookOrderData,
26
+ IndexPrice,
27
+ Kline,
28
+ KlineList,
29
+ MarkPrice,
30
+ Ticker,
31
+ Trade,
32
+ )
33
+
34
+
35
+ class DeribitPublicConnector(PublicConnector):
36
+ _ws_client: DeribitWSClient
37
+ _api_client: DeribitApiClient
38
+
39
+ def __init__(
40
+ self,
41
+ account_type: DeribitAccountType,
42
+ exchange: DeribitExchangeManager,
43
+ msgbus: MessageBus,
44
+ clock: LiveClock,
45
+ task_manager: TaskManager,
46
+ custom_url: str | None = None,
47
+ max_subscriptions_per_client: int | None = None,
48
+ max_clients: int | None = None,
49
+ **_: Any,
50
+ ):
51
+ super().__init__(
52
+ account_type=account_type,
53
+ market=exchange.market,
54
+ market_id=exchange.market_id,
55
+ exchange_id=exchange.exchange_id,
56
+ ws_client=DeribitWSClient(
57
+ account_type=account_type,
58
+ handler=self._ws_msg_handler,
59
+ task_manager=task_manager,
60
+ clock=clock,
61
+ custom_url=custom_url,
62
+ max_subscriptions_per_client=max_subscriptions_per_client,
63
+ max_clients=max_clients,
64
+ ),
65
+ msgbus=msgbus,
66
+ clock=clock,
67
+ api_client=DeribitApiClient(clock=clock, account_type=account_type),
68
+ task_manager=task_manager,
69
+ )
70
+
71
+ def _symbols(self, symbol: str | list[str]) -> list[tuple[str, str]]:
72
+ requested = [symbol] if isinstance(symbol, str) else symbol
73
+ result = []
74
+ for item in requested:
75
+ market = self._market.get(item)
76
+ if market is None:
77
+ raise ValueError(f"Unsupported Deribit symbol: {item}")
78
+ result.append((item, market.id))
79
+ return result
80
+
81
+ def _ws_msg_handler(self, raw: bytes) -> None:
82
+ try:
83
+ message = msgspec.json.decode(raw)
84
+ params = message.get("params", {})
85
+ channel = params.get("channel", "")
86
+ data = params.get("data")
87
+ if channel.startswith("trades."):
88
+ self._handle_trades(data)
89
+ elif channel.startswith("book."):
90
+ self._handle_book(channel, data)
91
+ elif channel.startswith("chart.trades."):
92
+ self._handle_kline(channel, data)
93
+ elif channel.startswith("ticker."):
94
+ self._handle_ticker(data)
95
+ except (KeyError, TypeError, ValueError, msgspec.DecodeError) as exc:
96
+ self._log.error(
97
+ "Error handling Deribit websocket message: %s: %s", raw, exc
98
+ )
99
+
100
+ def _handle_trades(self, data: list[dict[str, Any]]) -> None:
101
+ for item in data:
102
+ symbol = self._market_id[item["instrument_name"]]
103
+ self._msgbus.publish(
104
+ topic="trade",
105
+ msg=Trade(
106
+ exchange=self._exchange_id,
107
+ symbol=symbol,
108
+ side=DeribitEnumParser.parse_direction(item["direction"]),
109
+ price=float(item["price"]),
110
+ size=float(item["amount"]),
111
+ timestamp=int(item["timestamp"]),
112
+ ),
113
+ )
114
+
115
+ def _handle_book(self, channel: str, data: dict[str, Any]) -> None:
116
+ symbol = self._market_id[data["instrument_name"]]
117
+ bids = [
118
+ BookOrderData(price=float(px), size=float(size))
119
+ for px, size in data["bids"]
120
+ ]
121
+ asks = [
122
+ BookOrderData(price=float(px), size=float(size))
123
+ for px, size in data["asks"]
124
+ ]
125
+ timestamp = int(data["timestamp"])
126
+ depth = int(channel.split(".")[-2])
127
+ if depth == 1:
128
+ if not bids or not asks:
129
+ return
130
+ self._msgbus.publish(
131
+ topic="bookl1",
132
+ msg=BookL1(
133
+ exchange=self._exchange_id,
134
+ symbol=symbol,
135
+ bid=bids[0].price,
136
+ ask=asks[0].price,
137
+ bid_size=bids[0].size,
138
+ ask_size=asks[0].size,
139
+ timestamp=timestamp,
140
+ ),
141
+ )
142
+ else:
143
+ self._msgbus.publish(
144
+ topic="bookl2",
145
+ msg=BookL2(
146
+ exchange=self._exchange_id,
147
+ symbol=symbol,
148
+ bids=bids,
149
+ asks=asks,
150
+ timestamp=timestamp,
151
+ ),
152
+ )
153
+
154
+ def _handle_kline(self, channel: str, data: dict[str, Any]) -> None:
155
+ instrument_name = channel.split(".")[2]
156
+ resolution = channel.split(".")[3]
157
+ symbol = self._market_id[instrument_name]
158
+ interval = DeribitEnumParser.parse_resolution(resolution)
159
+ start = int(data["tick"])
160
+ self._msgbus.publish(
161
+ topic="kline",
162
+ msg=Kline(
163
+ exchange=self._exchange_id,
164
+ symbol=symbol,
165
+ interval=interval,
166
+ open=float(data["open"]),
167
+ high=float(data["high"]),
168
+ low=float(data["low"]),
169
+ close=float(data["close"]),
170
+ volume=float(data["volume"]),
171
+ start=start,
172
+ timestamp=self._clock.timestamp_ms(),
173
+ confirm=self._clock.timestamp_ms() >= start + interval.milliseconds,
174
+ ),
175
+ )
176
+
177
+ def _handle_ticker(self, data: dict[str, Any]) -> None:
178
+ symbol = self._market_id[data["instrument_name"]]
179
+ timestamp = int(data["timestamp"])
180
+ mark_price = data.get("mark_price")
181
+ index_price = data.get("index_price")
182
+ if mark_price is not None:
183
+ self._msgbus.publish(
184
+ topic="mark_price",
185
+ msg=MarkPrice(
186
+ exchange=self._exchange_id,
187
+ symbol=symbol,
188
+ price=float(mark_price),
189
+ timestamp=timestamp,
190
+ ),
191
+ )
192
+ if index_price is not None:
193
+ self._msgbus.publish(
194
+ topic="index_price",
195
+ msg=IndexPrice(
196
+ exchange=self._exchange_id,
197
+ symbol=symbol,
198
+ price=float(index_price),
199
+ timestamp=timestamp,
200
+ ),
201
+ )
202
+
203
+ async def request_ticker(self, symbol: str) -> Ticker:
204
+ market = self._market.get(symbol)
205
+ if market is None:
206
+ raise ValueError(f"Unsupported Deribit symbol: {symbol}")
207
+ result = await self._api_client.get_book_summary_by_instrument(market.id)
208
+ if not result:
209
+ raise ValueError(f"No Deribit ticker data for {symbol}")
210
+ data = result[0]
211
+ return Ticker(
212
+ exchange=self._exchange_id,
213
+ symbol=symbol,
214
+ last_price=float(data.get("last") or 0),
215
+ timestamp=int(data["creation_timestamp"]),
216
+ volume=float(data.get("volume") or 0),
217
+ volumeCcy=float(
218
+ (data.get("volume") if market.inverse else data.get("volume_notional"))
219
+ or 0
220
+ ),
221
+ )
222
+
223
+ async def request_all_tickers(self) -> dict[str, Ticker]:
224
+ currencies = {market.base for market in self._market.values()}
225
+ result: dict[str, Ticker] = {}
226
+ timestamp = self._clock.timestamp_ms()
227
+ for currency in currencies:
228
+ for item in await self._api_client.get_book_summary_by_currency(currency):
229
+ symbol = self._market_id.get(item["instrument_name"])
230
+ if symbol is None:
231
+ continue
232
+ result[symbol] = Ticker(
233
+ exchange=self._exchange_id,
234
+ symbol=symbol,
235
+ last_price=float(item.get("last") or 0),
236
+ timestamp=timestamp,
237
+ volume=float(item.get("volume") or 0),
238
+ volumeCcy=float(
239
+ (
240
+ item.get("volume")
241
+ if self._market[symbol].inverse
242
+ else item.get("volume_notional")
243
+ )
244
+ or 0
245
+ ),
246
+ )
247
+ return result
248
+
249
+ async def request_klines(
250
+ self,
251
+ symbol: str,
252
+ interval: KlineInterval,
253
+ limit: int | None = None,
254
+ start_time: int | None = None,
255
+ end_time: int | None = None,
256
+ ) -> KlineList:
257
+ market = self._market.get(symbol)
258
+ if market is None:
259
+ raise ValueError(f"Unsupported Deribit symbol: {symbol}")
260
+ end = end_time or self._clock.timestamp_ms()
261
+ count = limit or 1000
262
+ start = (
263
+ start_time
264
+ if start_time is not None
265
+ else end - count * interval.milliseconds
266
+ )
267
+ data = await self._api_client.get_chart_data(
268
+ market.id, DeribitEnumParser.to_resolution(interval), start, end
269
+ )
270
+ klines = []
271
+ for index, tick in enumerate(data.get("ticks", [])):
272
+ klines.append(
273
+ Kline(
274
+ exchange=self._exchange_id,
275
+ symbol=symbol,
276
+ interval=interval,
277
+ open=float(data["open"][index]),
278
+ high=float(data["high"][index]),
279
+ low=float(data["low"][index]),
280
+ close=float(data["close"][index]),
281
+ volume=float(data["volume"][index]),
282
+ start=int(tick),
283
+ timestamp=end,
284
+ confirm=end >= int(tick) + interval.milliseconds,
285
+ )
286
+ )
287
+ if limit is not None:
288
+ klines = klines[-limit:]
289
+ return KlineList(klines)
290
+
291
+ async def request_index_klines(
292
+ self,
293
+ symbol: str,
294
+ interval: KlineInterval,
295
+ limit: int | None = None,
296
+ start_time: int | None = None,
297
+ end_time: int | None = None,
298
+ ) -> KlineList:
299
+ raise NotImplementedError("Deribit index klines are not supported")
300
+
301
+ def subscribe_trade(self, symbol: str | list[str]):
302
+ self._ws_client.subscribe(
303
+ [f"trades.{item}.100ms" for _, item in self._symbols(symbol)]
304
+ )
305
+
306
+ def unsubscribe_trade(self, symbol: str | list[str]):
307
+ self._ws_client.unsubscribe(
308
+ [f"trades.{item}.100ms" for _, item in self._symbols(symbol)]
309
+ )
310
+
311
+ def subscribe_bookl1(self, symbol: str | list[str]):
312
+ self._ws_client.subscribe(
313
+ [f"book.{item}.none.1.100ms" for _, item in self._symbols(symbol)]
314
+ )
315
+
316
+ def unsubscribe_bookl1(self, symbol: str | list[str]):
317
+ self._ws_client.unsubscribe(
318
+ [f"book.{item}.none.1.100ms" for _, item in self._symbols(symbol)]
319
+ )
320
+
321
+ def subscribe_bookl2(self, symbol: str | list[str], level: BookLevel):
322
+ if level not in {BookLevel.L10, BookLevel.L20}:
323
+ raise ValueError(
324
+ f"Deribit only supports BookLevel.L10 and BookLevel.L20, got {level}"
325
+ )
326
+ self._ws_client.subscribe(
327
+ [
328
+ f"book.{item}.none.{level.value}.100ms"
329
+ for _, item in self._symbols(symbol)
330
+ ]
331
+ )
332
+
333
+ def unsubscribe_bookl2(self, symbol: str | list[str], level: BookLevel):
334
+ if level not in {BookLevel.L10, BookLevel.L20}:
335
+ raise ValueError(
336
+ f"Deribit only supports BookLevel.L10 and BookLevel.L20, got {level}"
337
+ )
338
+ self._ws_client.unsubscribe(
339
+ [
340
+ f"book.{item}.none.{level.value}.100ms"
341
+ for _, item in self._symbols(symbol)
342
+ ]
343
+ )
344
+
345
+ def subscribe_kline(self, symbol: str | list[str], interval: KlineInterval):
346
+ resolution = DeribitEnumParser.to_resolution(interval)
347
+ self._ws_client.subscribe(
348
+ [f"chart.trades.{item}.{resolution}" for _, item in self._symbols(symbol)]
349
+ )
350
+
351
+ def unsubscribe_kline(self, symbol: str | list[str], interval: KlineInterval):
352
+ resolution = DeribitEnumParser.to_resolution(interval)
353
+ self._ws_client.unsubscribe(
354
+ [f"chart.trades.{item}.{resolution}" for _, item in self._symbols(symbol)]
355
+ )
356
+
357
+ def subscribe_mark_price(self, symbol: str | list[str]):
358
+ self._subscribe_tickers(symbol)
359
+
360
+ def unsubscribe_mark_price(self, symbol: str | list[str]):
361
+ self._unsubscribe_tickers(symbol)
362
+
363
+ def subscribe_index_price(self, symbol: str | list[str]):
364
+ self._subscribe_tickers(symbol)
365
+
366
+ def unsubscribe_index_price(self, symbol: str | list[str]):
367
+ self._unsubscribe_tickers(symbol)
368
+
369
+ def _subscribe_tickers(self, symbol: str | list[str]):
370
+ self._ws_client.subscribe(
371
+ [f"ticker.{item}.100ms" for _, item in self._symbols(symbol)]
372
+ )
373
+
374
+ def _unsubscribe_tickers(self, symbol: str | list[str]):
375
+ self._ws_client.unsubscribe(
376
+ [f"ticker.{item}.100ms" for _, item in self._symbols(symbol)]
377
+ )
378
+
379
+ def subscribe_funding_rate(self, symbol: str | list[str]):
380
+ raise NotImplementedError("Deribit funding rate is not supported")
381
+
382
+ def unsubscribe_funding_rate(self, symbol: str | list[str]):
383
+ raise NotImplementedError("Deribit funding rate is not supported")
384
+
385
+
386
+ class DeribitPrivateConnector(PrivateConnector):
387
+ def __init__(
388
+ self,
389
+ account_type: DeribitAccountType,
390
+ exchange: DeribitExchangeManager,
391
+ cache: AsyncCache,
392
+ registry: OrderRegistry,
393
+ clock: LiveClock,
394
+ msgbus: MessageBus,
395
+ task_manager: TaskManager,
396
+ order_query_config: OrderQueryConfig,
397
+ **kwargs: Any,
398
+ ):
399
+ if not exchange.api_key or not exchange.secret:
400
+ raise ValueError("Deribit client_id and client_secret are required")
401
+ api_client = DeribitApiClient(
402
+ clock=clock,
403
+ account_type=account_type,
404
+ api_key=exchange.api_key,
405
+ secret=exchange.secret,
406
+ )
407
+ oms = DeribitOrderManagementSystem(
408
+ account_type=account_type,
409
+ api_key=exchange.api_key,
410
+ secret=exchange.secret,
411
+ market=cast(dict, exchange.market),
412
+ market_id=exchange.market_id,
413
+ registry=registry,
414
+ cache=cache,
415
+ api_client=api_client,
416
+ exchange_id=exchange.exchange_id,
417
+ clock=clock,
418
+ msgbus=msgbus,
419
+ task_manager=task_manager,
420
+ order_query_config=order_query_config,
421
+ )
422
+ super().__init__(
423
+ account_type=account_type,
424
+ market=exchange.market,
425
+ api_client=api_client,
426
+ task_manager=task_manager,
427
+ oms=oms,
428
+ )
429
+
430
+ async def connect(self):
431
+ self._oms._ws_client.subscribe(
432
+ [
433
+ "user.changes.future.any.100ms",
434
+ "user.changes.spot.any.100ms",
435
+ "user.portfolio.any",
436
+ ]
437
+ )
438
+ await self._oms._ws_client.connect()
439
+ await self._oms._ws_api_client.connect()