walrasquant-lib 0.4.28__tar.gz → 0.5.0__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 (121) hide show
  1. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/PKG-INFO +1 -1
  2. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/pyproject.toml +1 -1
  3. walrasquant_lib-0.5.0/src/walrasquant/base/connector.py +448 -0
  4. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/base/ems.py +75 -119
  5. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/base/exchange.py +3 -3
  6. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/base/oms.py +129 -43
  7. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/base/sms.py +7 -6
  8. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/base/ws_client.py +13 -3
  9. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/constants.py +25 -0
  10. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/core/cache.py +55 -13
  11. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/core/nautilius_core.py +1 -1
  12. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/engine.py +10 -8
  13. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/binance/ems.py +10 -53
  14. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/binance/exchange.py +9 -39
  15. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/binance/oms.py +332 -178
  16. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/binance/rest_api.py +1 -1
  17. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/binance/schema.py +71 -1
  18. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/bitget/connector.py +4 -4
  19. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/bitget/ems.py +14 -72
  20. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/bitget/exchange.py +10 -8
  21. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/bitget/oms.py +261 -360
  22. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/bitget/schema.py +45 -2
  23. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/bybit/constants.py +0 -1
  24. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/bybit/ems.py +2 -32
  25. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/bybit/exchange.py +9 -7
  26. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/bybit/oms.py +283 -173
  27. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/bybit/rest_api.py +0 -1
  28. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/bybit/schema.py +17 -0
  29. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/deribit/connector.py +13 -2
  30. walrasquant_lib-0.5.0/src/walrasquant/exchange/deribit/constants.py +213 -0
  31. walrasquant_lib-0.5.0/src/walrasquant/exchange/deribit/ems.py +26 -0
  32. walrasquant_lib-0.5.0/src/walrasquant/exchange/deribit/error.py +19 -0
  33. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/deribit/exchange.py +1 -2
  34. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/deribit/factory.py +1 -0
  35. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/deribit/oms.py +177 -21
  36. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/deribit/rest_api.py +83 -11
  37. walrasquant_lib-0.5.0/src/walrasquant/exchange/deribit/schema.py +68 -0
  38. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/deribit/websockets.py +59 -14
  39. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/hyperliquid/constants.py +1 -5
  40. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/hyperliquid/ems.py +8 -32
  41. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/hyperliquid/exchange.py +1 -2
  42. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/hyperliquid/oms.py +293 -182
  43. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/hyperliquid/schema.py +8 -0
  44. walrasquant_lib-0.5.0/src/walrasquant/exchange/okx/ems.py +63 -0
  45. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/okx/exchange.py +29 -5
  46. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/okx/oms.py +329 -206
  47. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/okx/schema.py +58 -1
  48. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/execution/algorithm.py +25 -40
  49. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/schema.py +114 -91
  50. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/strategy.py +23 -47
  51. walrasquant_lib-0.4.28/src/walrasquant/base/connector.py +0 -873
  52. walrasquant_lib-0.4.28/src/walrasquant/exchange/deribit/constants.py +0 -112
  53. walrasquant_lib-0.4.28/src/walrasquant/exchange/deribit/ems.py +0 -66
  54. walrasquant_lib-0.4.28/src/walrasquant/exchange/deribit/schema.py +0 -41
  55. walrasquant_lib-0.4.28/src/walrasquant/exchange/okx/ems.py +0 -146
  56. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/README.md +0 -0
  57. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/__init__.py +0 -0
  58. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/aggregation.py +0 -0
  59. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/backends/__init__.py +0 -0
  60. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/backends/db.py +0 -0
  61. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/backends/db_memory.py +0 -0
  62. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/backends/db_postgresql.py +0 -0
  63. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/backends/db_sqlite.py +0 -0
  64. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/base/__init__.py +0 -0
  65. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/base/api_client.py +0 -0
  66. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/base/retry.py +0 -0
  67. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/config.py +0 -0
  68. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/core/__init__.py +0 -0
  69. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/core/clock.py +0 -0
  70. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/core/connection.py +0 -0
  71. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/core/entity.py +0 -0
  72. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/core/registry.py +0 -0
  73. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/error.py +0 -0
  74. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/__init__.py +0 -0
  75. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/base_factory.py +0 -0
  76. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/binance/__init__.py +0 -0
  77. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/binance/connector.py +0 -0
  78. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/binance/constants.py +0 -0
  79. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/binance/error.py +0 -0
  80. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/binance/factory.py +0 -0
  81. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/binance/websockets.py +0 -0
  82. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/bitget/__init__.py +0 -0
  83. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/bitget/constants.py +0 -0
  84. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/bitget/error.py +0 -0
  85. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/bitget/factory.py +0 -0
  86. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/bitget/rest_api.py +0 -0
  87. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/bitget/websockets.py +0 -0
  88. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/bybit/__init__.py +0 -0
  89. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/bybit/connector.py +0 -0
  90. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/bybit/error.py +0 -0
  91. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/bybit/factory.py +0 -0
  92. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/bybit/websockets.py +0 -0
  93. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/deribit/__init__.py +0 -0
  94. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/hyperliquid/__init__.py +0 -0
  95. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/hyperliquid/connector.py +0 -0
  96. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/hyperliquid/error.py +0 -0
  97. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/hyperliquid/factory.py +0 -0
  98. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/hyperliquid/rest_api.py +0 -0
  99. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/hyperliquid/signing.py +0 -0
  100. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/hyperliquid/websockets.py +0 -0
  101. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/okx/__init__.py +0 -0
  102. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/okx/connector.py +0 -0
  103. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/okx/constants.py +0 -0
  104. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/okx/error.py +0 -0
  105. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/okx/factory.py +0 -0
  106. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/okx/rest_api.py +0 -0
  107. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/okx/websockets.py +0 -0
  108. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/exchange/registry.py +0 -0
  109. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/execution/__init__.py +0 -0
  110. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/execution/algorithms/__init__.py +0 -0
  111. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/execution/algorithms/twap.py +0 -0
  112. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/execution/config.py +0 -0
  113. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/execution/constants.py +0 -0
  114. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/execution/schema.py +0 -0
  115. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/indicator.py +0 -0
  116. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/push.py +0 -0
  117. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/tools/__init__.py +0 -0
  118. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/tools/pm2_wrapper.py +0 -0
  119. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/web/__init__.py +0 -0
  120. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/src/walrasquant/web/app.py +0 -0
  121. {walrasquant_lib-0.4.28 → walrasquant_lib-0.5.0}/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.28
3
+ Version: 0.5.0
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.28"
3
+ version = "0.5.0"
4
4
  description = "fastest python trading bot"
5
5
  authors = [
6
6
  {name = "River-Shi",email = "nachuan.shi.quant@gmail.com"}
@@ -0,0 +1,448 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Dict, List, Literal, Mapping
3
+ import inspect
4
+ import nexuslog as logging
5
+
6
+ from walrasquant.base.oms import OrderManagementSystem
7
+ from walrasquant.base.ws_client import WSClient
8
+ from walrasquant.base.api_client import ApiClient
9
+ from walrasquant.aggregation import (
10
+ KlineAggregator,
11
+ TimeKlineAggregator,
12
+ VolumeKlineAggregator,
13
+ )
14
+ from walrasquant.schema import (
15
+ BaseMarket,
16
+ KlineList,
17
+ Ticker,
18
+ Trade,
19
+ )
20
+ from walrasquant.constants import ExchangeType, AccountType
21
+ from walrasquant.core.entity import TaskManager
22
+ from walrasquant.constants import (
23
+ KlineInterval,
24
+ BookLevel,
25
+ )
26
+ from walrasquant.core.nautilius_core import LiveClock, MessageBus
27
+
28
+
29
+ class ApiProxy:
30
+ def __init__(self, api_client: ApiClient, task_manager: TaskManager):
31
+ self._api_client = api_client
32
+ self._task_manager = task_manager
33
+
34
+ def __getattr__(self, name):
35
+ if not hasattr(self._api_client, name):
36
+ raise AttributeError(f"ApiClient has no attribute '{name}'")
37
+
38
+ api_method = getattr(self._api_client, name)
39
+ if not callable(api_method):
40
+ return api_method
41
+
42
+ # check whether the method is async
43
+ if not inspect.iscoroutinefunction(api_method):
44
+ return api_method
45
+
46
+ def sync_wrapper(*args, **kwargs):
47
+ return self._task_manager.run_sync(api_method(*args, **kwargs))
48
+
49
+ return sync_wrapper
50
+
51
+
52
+ class FireApiProxy:
53
+ """Fire-and-forget proxy: submits async REST calls as background tasks, no return value."""
54
+
55
+ def __init__(self, api_client: ApiClient, task_manager: TaskManager):
56
+ self._api_client = api_client
57
+ self._task_manager = task_manager
58
+
59
+ def __getattr__(self, name):
60
+ if not hasattr(self._api_client, name):
61
+ raise AttributeError(f"ApiClient has no attribute '{name}'")
62
+
63
+ api_method = getattr(self._api_client, name)
64
+ if not inspect.iscoroutinefunction(api_method):
65
+ return api_method
66
+
67
+ def fire_wrapper(*args, **kwargs):
68
+ self._task_manager.create_task(api_method(*args, **kwargs))
69
+
70
+ return fire_wrapper
71
+
72
+
73
+ class PublicConnector(ABC):
74
+ def __init__(
75
+ self,
76
+ account_type: AccountType,
77
+ market: Mapping[str, BaseMarket],
78
+ market_id: Dict[str, str],
79
+ exchange_id: ExchangeType,
80
+ ws_client: WSClient,
81
+ msgbus: MessageBus,
82
+ clock: LiveClock,
83
+ api_client: ApiClient,
84
+ task_manager: TaskManager,
85
+ ):
86
+ self._log = logging.getLogger(name=type(self).__name__)
87
+ self._account_type = account_type
88
+ self._market = market
89
+ self._market_id = market_id
90
+ self._exchange_id = exchange_id
91
+ self._ws_client = ws_client
92
+ self._msgbus = msgbus
93
+ self._api_client = api_client
94
+ self._clock = clock
95
+ self._task_manager = task_manager
96
+
97
+ # Aggregator management - key: symbol, value: list of aggregators
98
+ self._aggregators: Dict[str, List[KlineAggregator]] = {}
99
+ self._msgbus.subscribe(
100
+ topic="trade", handler=self._handle_trade_for_aggregators
101
+ )
102
+
103
+ @property
104
+ def account_type(self):
105
+ return self._account_type
106
+
107
+ @abstractmethod
108
+ async def request_klines(
109
+ self,
110
+ symbol: str,
111
+ interval: KlineInterval,
112
+ limit: int | None = None,
113
+ start_time: int | None = None,
114
+ end_time: int | None = None,
115
+ ) -> KlineList:
116
+ """Request klines"""
117
+ pass
118
+
119
+ @abstractmethod
120
+ async def request_ticker(
121
+ self,
122
+ symbol: str,
123
+ ) -> Ticker:
124
+ """Request 24hr ticker data"""
125
+ pass
126
+
127
+ @abstractmethod
128
+ async def request_all_tickers(
129
+ self,
130
+ ) -> Dict[str, Ticker]:
131
+ """Request 24hr ticker data for multiple symbols"""
132
+ pass
133
+
134
+ @abstractmethod
135
+ async def request_index_klines(
136
+ self,
137
+ symbol: str,
138
+ interval: KlineInterval,
139
+ limit: int | None = None,
140
+ start_time: int | None = None,
141
+ end_time: int | None = None,
142
+ ) -> KlineList:
143
+ """Request index klines"""
144
+ pass
145
+
146
+ @abstractmethod
147
+ def subscribe_trade(self, symbol: str | List[str]):
148
+ """Subscribe to the trade data"""
149
+ pass
150
+
151
+ @abstractmethod
152
+ def unsubscribe_trade(self, symbol: str | List[str]):
153
+ """Unsubscribe from the trade data"""
154
+ pass
155
+
156
+ @abstractmethod
157
+ def subscribe_bookl1(self, symbol: str | List[str]):
158
+ """Subscribe to the bookl1 data"""
159
+ pass
160
+
161
+ @abstractmethod
162
+ def unsubscribe_bookl1(self, symbol: str | List[str]):
163
+ """Unsubscribe from the bookl1 data"""
164
+ pass
165
+
166
+ @abstractmethod
167
+ def subscribe_kline(
168
+ self,
169
+ symbol: str | List[str],
170
+ interval: KlineInterval,
171
+ ):
172
+ """Subscribe to the kline data
173
+
174
+ Args:
175
+ symbol: Symbol(s) to subscribe to
176
+ interval: Kline interval
177
+ use_aggregator: If True, use TimeKlineAggregator instead of exchange native klines
178
+ """
179
+ pass
180
+
181
+ @abstractmethod
182
+ def unsubscribe_kline(
183
+ self,
184
+ symbol: str | List[str],
185
+ interval: KlineInterval,
186
+ ):
187
+ """Unsubscribe from the kline data"""
188
+ pass
189
+
190
+ def subscribe_kline_aggregator(
191
+ self,
192
+ symbol: str,
193
+ interval: KlineInterval,
194
+ build_with_no_updates: bool,
195
+ ):
196
+ """Subscribe to time-based kline data using TimeKlineAggregator
197
+
198
+ Args:
199
+ symbol: Symbol to subscribe to
200
+ interval: Kline interval
201
+ """
202
+ # Ensure trade subscription for the symbol
203
+ self.subscribe_trade(symbol)
204
+
205
+ # Create and register time kline aggregator
206
+ aggregator = self._create_time_kline_aggregator(
207
+ symbol, interval, build_with_no_updates
208
+ )
209
+ self._add_aggregator(symbol, aggregator)
210
+
211
+ self._log.info(
212
+ "Time kline aggregator created for %s with interval %s", symbol, interval
213
+ )
214
+
215
+ def unsubscribe_kline_aggregator(
216
+ self,
217
+ symbol: str,
218
+ interval: KlineInterval,
219
+ ):
220
+ """Unsubscribe from time-based kline data using TimeKlineAggregator
221
+
222
+ Args:
223
+ symbol: Symbol to unsubscribe from
224
+ interval: Kline interval
225
+ """
226
+ aggregators = self._aggregators.get(symbol, [])
227
+ # Create a copy to avoid "list changed size during iteration" error
228
+ for aggregator in aggregators.copy():
229
+ if (
230
+ isinstance(aggregator, TimeKlineAggregator)
231
+ and aggregator.interval == interval
232
+ ):
233
+ aggregator.stop()
234
+ aggregators.remove(aggregator)
235
+ self._log.info(
236
+ "Time kline aggregator stopped for %s with interval %s",
237
+ symbol,
238
+ interval,
239
+ )
240
+ if not aggregators:
241
+ self._aggregators.pop(symbol, None)
242
+ self.unsubscribe_trade(symbol)
243
+
244
+ def subscribe_volume_kline_aggregator(
245
+ self,
246
+ symbol: str,
247
+ volume_threshold: float,
248
+ volume_type: Literal["BUY", "SELL", "DEFAULT"],
249
+ ):
250
+ """Subscribe to volume-based kline data using VolumeKlineAggregator
251
+
252
+ Args:
253
+ symbol: Symbol to subscribe to
254
+ volume_threshold: Volume threshold for creating new klines
255
+ """
256
+ # Ensure trade subscription for the symbol
257
+ self.subscribe_trade(symbol)
258
+
259
+ # Create and register volume kline aggregator
260
+ aggregator = self._create_volume_kline_aggregator(
261
+ symbol, volume_threshold, volume_type
262
+ )
263
+ self._add_aggregator(symbol, aggregator)
264
+
265
+ self._log.info(
266
+ "Volume kline aggregator created for %s with threshold %s and type %s",
267
+ symbol,
268
+ volume_threshold,
269
+ volume_type,
270
+ )
271
+
272
+ def unsubscribe_volume_kline_aggregator(
273
+ self, symbol: str, volume_threshold: float, volume_type: str
274
+ ):
275
+ """Unsubscribe from volume-based kline data using VolumeKlineAggregator
276
+
277
+ Args:
278
+ symbol: Symbol to unsubscribe from
279
+ volume_threshold: Volume threshold for the kline aggregator
280
+ """
281
+ aggregators = self._aggregators.get(symbol, [])
282
+ # Create a copy to avoid "list changed size during iteration" error
283
+ for aggregator in aggregators.copy():
284
+ if (
285
+ isinstance(aggregator, VolumeKlineAggregator)
286
+ and aggregator.volume_threshold == volume_threshold
287
+ ):
288
+ aggregators.remove(aggregator)
289
+ self._log.info(
290
+ "Volume kline aggregator stopped for %s with threshold %s and type %s",
291
+ symbol,
292
+ volume_threshold,
293
+ volume_type,
294
+ )
295
+ if not aggregators:
296
+ self._aggregators.pop(symbol, None)
297
+ self.unsubscribe_trade(symbol)
298
+
299
+ @abstractmethod
300
+ def subscribe_bookl2(self, symbol: str | List[str], level: BookLevel):
301
+ """Subscribe to the bookl2 data"""
302
+ pass
303
+
304
+ @abstractmethod
305
+ def unsubscribe_bookl2(self, symbol: str | List[str], level: BookLevel):
306
+ """Unsubscribe from the bookl2 data"""
307
+ pass
308
+
309
+ @abstractmethod
310
+ def subscribe_funding_rate(self, symbol: str | List[str]):
311
+ """Subscribe to the funding rate data"""
312
+ pass
313
+
314
+ @abstractmethod
315
+ def unsubscribe_funding_rate(self, symbol: str | List[str]):
316
+ """Unsubscribe from the funding rate data"""
317
+ pass
318
+
319
+ @abstractmethod
320
+ def subscribe_index_price(self, symbol: str | List[str]):
321
+ """Subscribe to the index price data"""
322
+ pass
323
+
324
+ @abstractmethod
325
+ def unsubscribe_index_price(self, symbol: str | List[str]):
326
+ """Unsubscribe from the index price data"""
327
+ pass
328
+
329
+ @abstractmethod
330
+ def subscribe_mark_price(self, symbol: str | List[str]):
331
+ """Subscribe to the mark price data"""
332
+ pass
333
+
334
+ @abstractmethod
335
+ def unsubscribe_mark_price(self, symbol: str | List[str]):
336
+ """Unsubscribe from the mark price data"""
337
+ pass
338
+
339
+ def _create_time_kline_aggregator(
340
+ self,
341
+ symbol: str,
342
+ interval: KlineInterval,
343
+ build_with_no_updates: bool,
344
+ ) -> TimeKlineAggregator:
345
+ """Create a time-based kline aggregator."""
346
+ return TimeKlineAggregator(
347
+ exchange=self._exchange_id,
348
+ symbol=symbol,
349
+ interval=interval,
350
+ msgbus=self._msgbus,
351
+ clock=self._clock,
352
+ build_with_no_updates=build_with_no_updates,
353
+ )
354
+
355
+ def _create_volume_kline_aggregator(
356
+ self,
357
+ symbol: str,
358
+ volume_threshold: float,
359
+ volume_type: Literal["BUY", "SELL", "DEFAULT"],
360
+ ) -> VolumeKlineAggregator:
361
+ """Create a volume-based kline aggregator."""
362
+
363
+ return VolumeKlineAggregator(
364
+ exchange=self._exchange_id,
365
+ symbol=symbol,
366
+ msgbus=self._msgbus,
367
+ volume_threshold=volume_threshold,
368
+ volume_type=volume_type,
369
+ )
370
+
371
+ def _add_aggregator(self, symbol: str, aggregator) -> None:
372
+ """Add an aggregator for a symbol."""
373
+ if symbol not in self._aggregators:
374
+ self._aggregators[symbol] = []
375
+ self._aggregators[symbol].append(aggregator)
376
+
377
+ def _handle_trade_for_aggregators(self, trade: Trade) -> None:
378
+ """Route trade data to all aggregators for this symbol."""
379
+ aggregators = self._aggregators.get(trade.symbol)
380
+ if aggregators:
381
+ for aggregator in aggregators:
382
+ aggregator.handle_trade(trade)
383
+
384
+ async def connect(self):
385
+ """Connect to the exchange"""
386
+ await self._ws_client.connect()
387
+
388
+ async def wait_ready(self):
389
+ """Wait for the initial WebSocket connection to be established"""
390
+ await self._ws_client.wait_ready()
391
+
392
+ async def disconnect(self):
393
+ """Disconnect from the exchange"""
394
+ # Stop all aggregators
395
+ for aggregators in self._aggregators.values():
396
+ for aggregator in aggregators:
397
+ if hasattr(aggregator, "stop"):
398
+ aggregator.stop() # type: ignore
399
+ self._aggregators.clear()
400
+
401
+ # NOTE: no need to manually disconnect ws_client here
402
+ # self._ws_client.disconnect() # not needed to await
403
+ await self._api_client.close_session()
404
+
405
+
406
+ class PrivateConnector(ABC):
407
+ def __init__(
408
+ self,
409
+ account_type: AccountType,
410
+ market: Mapping[str, BaseMarket],
411
+ api_client: ApiClient,
412
+ task_manager: TaskManager,
413
+ oms: OrderManagementSystem,
414
+ ):
415
+ self._log = logging.getLogger(name=type(self).__name__)
416
+ self._account_type = account_type
417
+ self._market = market
418
+ self._api_client = api_client
419
+ self._oms = oms
420
+ self._task_manager = task_manager
421
+ self._api_proxy = ApiProxy(self._api_client, self._task_manager)
422
+ self._api_fire_proxy = FireApiProxy(self._api_client, self._task_manager)
423
+
424
+ @property
425
+ def account_type(self):
426
+ return self._account_type
427
+
428
+ @property
429
+ def api(self):
430
+ return self._api_proxy
431
+
432
+ @property
433
+ def api_fire(self):
434
+ return self._api_fire_proxy
435
+
436
+ @abstractmethod
437
+ async def connect(self):
438
+ """Connect to the exchange"""
439
+ pass
440
+
441
+ async def wait_ready(self):
442
+ """Wait for the OMS WebSocket client(s) to be ready"""
443
+ await self._oms.wait_ready()
444
+
445
+ async def disconnect(self):
446
+ """Disconnect from the exchange"""
447
+ # self._oms._ws_client.disconnect() # not needed to await
448
+ await self._api_client.close_session()