walrasquant-lib 0.4.20__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- walrasquant/__init__.py +7 -0
- walrasquant/aggregation.py +449 -0
- walrasquant/backends/__init__.py +5 -0
- walrasquant/backends/db.py +109 -0
- walrasquant/backends/db_memory.py +61 -0
- walrasquant/backends/db_postgresql.py +321 -0
- walrasquant/backends/db_sqlite.py +310 -0
- walrasquant/base/__init__.py +24 -0
- walrasquant/base/api_client.py +46 -0
- walrasquant/base/connector.py +863 -0
- walrasquant/base/ems.py +794 -0
- walrasquant/base/exchange.py +213 -0
- walrasquant/base/oms.py +428 -0
- walrasquant/base/retry.py +220 -0
- walrasquant/base/sms.py +545 -0
- walrasquant/base/ws_client.py +408 -0
- walrasquant/config.py +284 -0
- walrasquant/constants.py +413 -0
- walrasquant/core/__init__.py +0 -0
- walrasquant/core/cache.py +688 -0
- walrasquant/core/clock.py +59 -0
- walrasquant/core/connection.py +41 -0
- walrasquant/core/entity.py +504 -0
- walrasquant/core/nautilius_core.py +103 -0
- walrasquant/core/registry.py +41 -0
- walrasquant/engine.py +745 -0
- walrasquant/error.py +34 -0
- walrasquant/exchange/__init__.py +13 -0
- walrasquant/exchange/base_factory.py +172 -0
- walrasquant/exchange/binance/__init__.py +30 -0
- walrasquant/exchange/binance/connector.py +1093 -0
- walrasquant/exchange/binance/constants.py +934 -0
- walrasquant/exchange/binance/ems.py +140 -0
- walrasquant/exchange/binance/error.py +48 -0
- walrasquant/exchange/binance/exchange.py +144 -0
- walrasquant/exchange/binance/factory.py +115 -0
- walrasquant/exchange/binance/oms.py +1807 -0
- walrasquant/exchange/binance/rest_api.py +1653 -0
- walrasquant/exchange/binance/schema.py +1063 -0
- walrasquant/exchange/binance/websockets.py +389 -0
- walrasquant/exchange/bitget/__init__.py +28 -0
- walrasquant/exchange/bitget/connector.py +578 -0
- walrasquant/exchange/bitget/constants.py +392 -0
- walrasquant/exchange/bitget/ems.py +202 -0
- walrasquant/exchange/bitget/error.py +36 -0
- walrasquant/exchange/bitget/exchange.py +128 -0
- walrasquant/exchange/bitget/factory.py +135 -0
- walrasquant/exchange/bitget/oms.py +1619 -0
- walrasquant/exchange/bitget/rest_api.py +610 -0
- walrasquant/exchange/bitget/schema.py +885 -0
- walrasquant/exchange/bitget/websockets.py +753 -0
- walrasquant/exchange/bybit/__init__.py +32 -0
- walrasquant/exchange/bybit/connector.py +819 -0
- walrasquant/exchange/bybit/constants.py +479 -0
- walrasquant/exchange/bybit/ems.py +93 -0
- walrasquant/exchange/bybit/error.py +36 -0
- walrasquant/exchange/bybit/exchange.py +108 -0
- walrasquant/exchange/bybit/factory.py +128 -0
- walrasquant/exchange/bybit/oms.py +1195 -0
- walrasquant/exchange/bybit/rest_api.py +570 -0
- walrasquant/exchange/bybit/schema.py +867 -0
- walrasquant/exchange/bybit/websockets.py +307 -0
- walrasquant/exchange/hyperliquid/__init__.py +28 -0
- walrasquant/exchange/hyperliquid/connector.py +370 -0
- walrasquant/exchange/hyperliquid/constants.py +371 -0
- walrasquant/exchange/hyperliquid/ems.py +156 -0
- walrasquant/exchange/hyperliquid/error.py +48 -0
- walrasquant/exchange/hyperliquid/exchange.py +120 -0
- walrasquant/exchange/hyperliquid/factory.py +135 -0
- walrasquant/exchange/hyperliquid/oms.py +1081 -0
- walrasquant/exchange/hyperliquid/rest_api.py +348 -0
- walrasquant/exchange/hyperliquid/schema.py +583 -0
- walrasquant/exchange/hyperliquid/websockets.py +592 -0
- walrasquant/exchange/okx/__init__.py +25 -0
- walrasquant/exchange/okx/connector.py +931 -0
- walrasquant/exchange/okx/constants.py +518 -0
- walrasquant/exchange/okx/ems.py +144 -0
- walrasquant/exchange/okx/error.py +66 -0
- walrasquant/exchange/okx/exchange.py +102 -0
- walrasquant/exchange/okx/factory.py +138 -0
- walrasquant/exchange/okx/oms.py +1199 -0
- walrasquant/exchange/okx/rest_api.py +799 -0
- walrasquant/exchange/okx/schema.py +1449 -0
- walrasquant/exchange/okx/websockets.py +420 -0
- walrasquant/exchange/registry.py +201 -0
- walrasquant/execution/__init__.py +24 -0
- walrasquant/execution/algorithm.py +968 -0
- walrasquant/execution/algorithms/__init__.py +3 -0
- walrasquant/execution/algorithms/twap.py +392 -0
- walrasquant/execution/config.py +34 -0
- walrasquant/execution/constants.py +27 -0
- walrasquant/execution/schema.py +62 -0
- walrasquant/indicator.py +382 -0
- walrasquant/push.py +77 -0
- walrasquant/schema.py +755 -0
- walrasquant/strategy.py +1805 -0
- walrasquant/tools/__init__.py +0 -0
- walrasquant/tools/pm2_wrapper.py +1016 -0
- walrasquant/web/__init__.py +26 -0
- walrasquant/web/app.py +157 -0
- walrasquant/web/server.py +92 -0
- walrasquant_lib-0.4.20.dist-info/METADATA +162 -0
- walrasquant_lib-0.4.20.dist-info/RECORD +105 -0
- walrasquant_lib-0.4.20.dist-info/WHEEL +4 -0
- walrasquant_lib-0.4.20.dist-info/entry_points.txt +3 -0
walrasquant/__init__.py
ADDED
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Kline aggregation for live trading.
|
|
3
|
+
|
|
4
|
+
This module provides kline (candlestick) aggregation from trade data.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Literal
|
|
8
|
+
import nexuslog as logging
|
|
9
|
+
from datetime import timedelta, datetime, timezone
|
|
10
|
+
from walrasquant.schema import Trade, Kline
|
|
11
|
+
from walrasquant.constants import KlineInterval, ExchangeType, OrderSide
|
|
12
|
+
from walrasquant.core.nautilius_core import LiveClock, TimeEvent, MessageBus
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class KlineBuilder:
|
|
16
|
+
"""
|
|
17
|
+
Kline builder for aggregating trade data into OHLCV klines.
|
|
18
|
+
|
|
19
|
+
Parameters
|
|
20
|
+
----------
|
|
21
|
+
exchange : ExchangeType
|
|
22
|
+
The exchange for the klines
|
|
23
|
+
symbol : str
|
|
24
|
+
The symbol for the klines
|
|
25
|
+
interval : KlineInterval
|
|
26
|
+
The kline interval
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
exchange: ExchangeType,
|
|
32
|
+
symbol: str,
|
|
33
|
+
interval: KlineInterval,
|
|
34
|
+
):
|
|
35
|
+
self.exchange = exchange
|
|
36
|
+
self.symbol = symbol
|
|
37
|
+
self.interval = interval
|
|
38
|
+
|
|
39
|
+
# OHLCV data as member variables
|
|
40
|
+
self._open: float | None = None
|
|
41
|
+
self._high: float | None = None
|
|
42
|
+
self._low: float | None = None
|
|
43
|
+
self._close: float | None = None
|
|
44
|
+
self._volume = 0.0
|
|
45
|
+
self._buy_volume = 0.0
|
|
46
|
+
self._last_close: float | None = None
|
|
47
|
+
|
|
48
|
+
self.initialized = False
|
|
49
|
+
self.ts_last = 0
|
|
50
|
+
self.count = 0
|
|
51
|
+
|
|
52
|
+
def __repr__(self) -> str:
|
|
53
|
+
return (
|
|
54
|
+
f"{type(self).__name__}("
|
|
55
|
+
f"{self.exchange.value}, "
|
|
56
|
+
f"{self.symbol}, "
|
|
57
|
+
f"{self.interval.value}, "
|
|
58
|
+
f"open={self._open}, "
|
|
59
|
+
f"high={self._high}, "
|
|
60
|
+
f"low={self._low}, "
|
|
61
|
+
f"close={self._close}, "
|
|
62
|
+
f"volume={self._volume}, "
|
|
63
|
+
f"buy_volume={self._buy_volume})"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
def update(self, trade: Trade) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Update the kline builder with a new trade.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
trade : Trade
|
|
73
|
+
The trade data to update with
|
|
74
|
+
"""
|
|
75
|
+
if trade.timestamp < self.ts_last:
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
if trade.price <= 0:
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
if self._open is None:
|
|
82
|
+
self._open = trade.price
|
|
83
|
+
self._high = trade.price
|
|
84
|
+
self._low = trade.price
|
|
85
|
+
self.initialized = True
|
|
86
|
+
else:
|
|
87
|
+
if self._high is not None and trade.price > self._high:
|
|
88
|
+
self._high = trade.price
|
|
89
|
+
if self._low is not None and trade.price < self._low:
|
|
90
|
+
self._low = trade.price
|
|
91
|
+
|
|
92
|
+
self._close = trade.price
|
|
93
|
+
self._volume += trade.size
|
|
94
|
+
if trade.side.is_buy:
|
|
95
|
+
self._buy_volume += trade.size
|
|
96
|
+
self.count += 1
|
|
97
|
+
self.ts_last = trade.timestamp
|
|
98
|
+
|
|
99
|
+
def reset(self) -> None:
|
|
100
|
+
"""Reset the builder to initial state."""
|
|
101
|
+
self._open = None
|
|
102
|
+
self._high = None
|
|
103
|
+
self._low = None
|
|
104
|
+
self._volume = 0.0
|
|
105
|
+
self._buy_volume = 0.0
|
|
106
|
+
self.count = 0
|
|
107
|
+
|
|
108
|
+
def build(self, start: int, timestamp: int) -> Kline | None:
|
|
109
|
+
"""
|
|
110
|
+
Build a kline from the current state and reset.
|
|
111
|
+
|
|
112
|
+
Returns None if no trades were received (not initialized).
|
|
113
|
+
|
|
114
|
+
Parameters
|
|
115
|
+
----------
|
|
116
|
+
ts_event : int
|
|
117
|
+
Timestamp (nanoseconds) for the kline event
|
|
118
|
+
ts_init : int
|
|
119
|
+
Timestamp (nanoseconds) for the kline initialization
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
Kline | None
|
|
124
|
+
The built kline, or None if no trades received
|
|
125
|
+
"""
|
|
126
|
+
# If no trades received, don't emit a kline
|
|
127
|
+
if not self.initialized:
|
|
128
|
+
return None
|
|
129
|
+
|
|
130
|
+
if self._open is None:
|
|
131
|
+
self._open = self._last_close
|
|
132
|
+
self._high = self._last_close
|
|
133
|
+
self._low = self._last_close
|
|
134
|
+
self._close = self._last_close
|
|
135
|
+
|
|
136
|
+
if (
|
|
137
|
+
self._open is None
|
|
138
|
+
or self._high is None
|
|
139
|
+
or self._low is None
|
|
140
|
+
or self._close is None
|
|
141
|
+
):
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
self._low = min(self._low, self._close)
|
|
145
|
+
self._high = max(self._high, self._close)
|
|
146
|
+
|
|
147
|
+
kline = Kline(
|
|
148
|
+
exchange=self.exchange,
|
|
149
|
+
symbol=self.symbol,
|
|
150
|
+
interval=self.interval,
|
|
151
|
+
open=self._open,
|
|
152
|
+
high=self._high,
|
|
153
|
+
low=self._low,
|
|
154
|
+
close=self._close,
|
|
155
|
+
volume=self._volume,
|
|
156
|
+
buy_volume=self._buy_volume,
|
|
157
|
+
start=start,
|
|
158
|
+
timestamp=timestamp,
|
|
159
|
+
confirm=True,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
self._last_close = self._close
|
|
163
|
+
self.reset()
|
|
164
|
+
return kline
|
|
165
|
+
|
|
166
|
+
def build_now(self) -> Kline | None:
|
|
167
|
+
"""
|
|
168
|
+
Build a kline with current timestamp and reset.
|
|
169
|
+
|
|
170
|
+
Returns None if no trades were received.
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
Kline | None
|
|
175
|
+
The built kline, or None if no trades received
|
|
176
|
+
"""
|
|
177
|
+
return self.build(self.ts_last, self.ts_last)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class KlineAggregator:
|
|
181
|
+
"""
|
|
182
|
+
Base class for kline aggregation from trade data.
|
|
183
|
+
|
|
184
|
+
Parameters
|
|
185
|
+
----------
|
|
186
|
+
exchange : ExchangeType
|
|
187
|
+
The exchange for the aggregator
|
|
188
|
+
symbol : str
|
|
189
|
+
The symbol for the aggregator
|
|
190
|
+
interval : KlineInterval
|
|
191
|
+
The kline interval
|
|
192
|
+
msgbus : MessageBus
|
|
193
|
+
The message bus for publishing klines
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
def __init__(
|
|
197
|
+
self,
|
|
198
|
+
exchange: ExchangeType,
|
|
199
|
+
symbol: str,
|
|
200
|
+
msgbus: MessageBus,
|
|
201
|
+
interval: KlineInterval,
|
|
202
|
+
):
|
|
203
|
+
self.exchange = exchange
|
|
204
|
+
self.symbol = symbol
|
|
205
|
+
self.interval = interval
|
|
206
|
+
self._msgbus = msgbus
|
|
207
|
+
self._log = logging.getLogger(name=type(self).__name__)
|
|
208
|
+
|
|
209
|
+
self._builder = KlineBuilder(
|
|
210
|
+
exchange=exchange,
|
|
211
|
+
symbol=symbol,
|
|
212
|
+
interval=interval,
|
|
213
|
+
)
|
|
214
|
+
self.is_running = False
|
|
215
|
+
|
|
216
|
+
def handle_trade(self, trade: Trade) -> None:
|
|
217
|
+
"""
|
|
218
|
+
Handle incoming trade data.
|
|
219
|
+
|
|
220
|
+
Parameters
|
|
221
|
+
----------
|
|
222
|
+
trade : Trade
|
|
223
|
+
The trade to process
|
|
224
|
+
"""
|
|
225
|
+
self._apply_update(trade)
|
|
226
|
+
|
|
227
|
+
def _apply_update(self, trade: Trade) -> None:
|
|
228
|
+
"""
|
|
229
|
+
Apply trade update to the aggregator.
|
|
230
|
+
|
|
231
|
+
Must be implemented by subclasses.
|
|
232
|
+
|
|
233
|
+
Parameters
|
|
234
|
+
----------
|
|
235
|
+
trade : Trade
|
|
236
|
+
The trade to process
|
|
237
|
+
"""
|
|
238
|
+
raise NotImplementedError(
|
|
239
|
+
"method `_apply_update` must be implemented in the subclass"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
def _build_now_and_send(self) -> None:
|
|
243
|
+
"""Build kline with current timestamp and publish to msgbus if kline was built."""
|
|
244
|
+
kline = self._builder.build_now()
|
|
245
|
+
if kline is not None:
|
|
246
|
+
self._msgbus.publish(topic="kline", msg=kline)
|
|
247
|
+
|
|
248
|
+
def _build_and_send(self, start: int, timestamp: int) -> None:
|
|
249
|
+
"""Build kline with specified timestamps and publish to msgbus if kline was built."""
|
|
250
|
+
kline = self._builder.build(start, timestamp)
|
|
251
|
+
if kline is not None:
|
|
252
|
+
self._msgbus.publish(topic="kline", msg=kline)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class VolumeKlineAggregator(KlineAggregator):
|
|
256
|
+
"""
|
|
257
|
+
Volume-based kline aggregator.
|
|
258
|
+
|
|
259
|
+
Creates klines when cumulative trade volume reaches the threshold.
|
|
260
|
+
Large trades are split across multiple klines.
|
|
261
|
+
|
|
262
|
+
Parameters
|
|
263
|
+
----------
|
|
264
|
+
exchange : ExchangeType
|
|
265
|
+
The exchange for the aggregator
|
|
266
|
+
symbol : str
|
|
267
|
+
The symbol for the aggregator
|
|
268
|
+
interval : KlineInterval
|
|
269
|
+
The kline interval (used for metadata only)
|
|
270
|
+
msgbus : MessageBus
|
|
271
|
+
The message bus for publishing klines
|
|
272
|
+
volume_threshold : float
|
|
273
|
+
Volume threshold for creating new klines
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
def __init__(
|
|
277
|
+
self,
|
|
278
|
+
exchange: ExchangeType,
|
|
279
|
+
symbol: str,
|
|
280
|
+
msgbus: MessageBus,
|
|
281
|
+
volume_threshold: float,
|
|
282
|
+
volume_type: Literal["DEFAULT", "BUY", "SELL"] = "DEFAULT",
|
|
283
|
+
):
|
|
284
|
+
super().__init__(exchange, symbol, msgbus, interval=KlineInterval.VOLUME)
|
|
285
|
+
self.volume_threshold = volume_threshold
|
|
286
|
+
|
|
287
|
+
if volume_type == "DEFAULT":
|
|
288
|
+
self._side = None
|
|
289
|
+
elif volume_type == "BUY":
|
|
290
|
+
self._side = OrderSide.BUY
|
|
291
|
+
elif volume_type == "SELL":
|
|
292
|
+
self._side = OrderSide.SELL
|
|
293
|
+
else:
|
|
294
|
+
raise ValueError(f"Invalid volume_type: {volume_type}")
|
|
295
|
+
|
|
296
|
+
def _apply_update(self, trade: Trade) -> None:
|
|
297
|
+
"""
|
|
298
|
+
Apply trade update with volume-based aggregation.
|
|
299
|
+
|
|
300
|
+
Parameters
|
|
301
|
+
----------
|
|
302
|
+
trade : Trade
|
|
303
|
+
The trade to process
|
|
304
|
+
"""
|
|
305
|
+
if self._side is not None and trade.side != self._side:
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
size_update = trade.size
|
|
309
|
+
|
|
310
|
+
while size_update > 0:
|
|
311
|
+
current_volume = self._builder._volume
|
|
312
|
+
|
|
313
|
+
if current_volume + size_update < self.volume_threshold:
|
|
314
|
+
# Update and break
|
|
315
|
+
partial_trade = Trade(
|
|
316
|
+
exchange=trade.exchange,
|
|
317
|
+
symbol=trade.symbol,
|
|
318
|
+
price=trade.price,
|
|
319
|
+
size=size_update,
|
|
320
|
+
side=trade.side,
|
|
321
|
+
timestamp=trade.timestamp,
|
|
322
|
+
)
|
|
323
|
+
self._builder.update(partial_trade)
|
|
324
|
+
break
|
|
325
|
+
|
|
326
|
+
# Calculate size needed to reach threshold
|
|
327
|
+
size_diff = self.volume_threshold - current_volume
|
|
328
|
+
|
|
329
|
+
# Update builder to threshold
|
|
330
|
+
partial_trade = Trade(
|
|
331
|
+
exchange=trade.exchange,
|
|
332
|
+
symbol=trade.symbol,
|
|
333
|
+
side=trade.side,
|
|
334
|
+
price=trade.price,
|
|
335
|
+
size=size_diff,
|
|
336
|
+
timestamp=trade.timestamp,
|
|
337
|
+
)
|
|
338
|
+
self._builder.update(partial_trade)
|
|
339
|
+
self._build_now_and_send()
|
|
340
|
+
|
|
341
|
+
# Decrement remaining size
|
|
342
|
+
size_update -= size_diff
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class TimeKlineAggregator(KlineAggregator):
|
|
346
|
+
"""
|
|
347
|
+
Time-based kline aggregator using LiveClock.
|
|
348
|
+
|
|
349
|
+
Creates klines at regular time intervals (1s to 1w).
|
|
350
|
+
|
|
351
|
+
Parameters
|
|
352
|
+
----------
|
|
353
|
+
exchange : ExchangeType
|
|
354
|
+
The exchange for the aggregator
|
|
355
|
+
symbol : str
|
|
356
|
+
The symbol for the aggregator
|
|
357
|
+
interval : KlineInterval
|
|
358
|
+
The kline interval
|
|
359
|
+
msgbus : MessageBus
|
|
360
|
+
The message bus for publishing klines
|
|
361
|
+
clock : LiveClock
|
|
362
|
+
The clock for timing
|
|
363
|
+
"""
|
|
364
|
+
|
|
365
|
+
def __init__(
|
|
366
|
+
self,
|
|
367
|
+
exchange: ExchangeType,
|
|
368
|
+
symbol: str,
|
|
369
|
+
interval: KlineInterval,
|
|
370
|
+
msgbus: MessageBus,
|
|
371
|
+
clock: LiveClock,
|
|
372
|
+
build_with_no_updates: bool = True,
|
|
373
|
+
):
|
|
374
|
+
super().__init__(exchange, symbol, msgbus, interval)
|
|
375
|
+
self._clock = clock
|
|
376
|
+
self._timer_name = f"{exchange.value}_{symbol}_{interval.value}"
|
|
377
|
+
|
|
378
|
+
self._interval_ms = interval.milliseconds
|
|
379
|
+
self._build_with_no_updates = build_with_no_updates
|
|
380
|
+
|
|
381
|
+
# Set up the timer
|
|
382
|
+
self._set_build_timer()
|
|
383
|
+
|
|
384
|
+
def _set_build_timer(self) -> None:
|
|
385
|
+
"""Set up the clock timer for kline building."""
|
|
386
|
+
start_time = self._get_start_time()
|
|
387
|
+
|
|
388
|
+
# Calculate interval timedelta
|
|
389
|
+
interval_td = timedelta(milliseconds=self._interval_ms)
|
|
390
|
+
|
|
391
|
+
self._clock.set_timer(
|
|
392
|
+
name=self._timer_name,
|
|
393
|
+
interval=interval_td,
|
|
394
|
+
start_time=start_time,
|
|
395
|
+
stop_time=None,
|
|
396
|
+
callback=self._build_bar,
|
|
397
|
+
)
|
|
398
|
+
self._log.debug(
|
|
399
|
+
f"Timer set: {self._timer_name}, start={start_time}, interval={interval_td}"
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
def _get_start_time(self):
|
|
403
|
+
"""
|
|
404
|
+
Calculate the start time for the next kline interval.
|
|
405
|
+
|
|
406
|
+
Uses the same logic as Nautilus TimeBarAggregator:
|
|
407
|
+
- Find the floor time (closest smaller time aligned to interval)
|
|
408
|
+
- Always schedule for the NEXT interval boundary
|
|
409
|
+
"""
|
|
410
|
+
timestamp = self._clock.timestamp_ms()
|
|
411
|
+
interval_ms = self._interval_ms
|
|
412
|
+
floored_timestamp = (timestamp // interval_ms) * interval_ms
|
|
413
|
+
start_time = datetime.fromtimestamp(floored_timestamp / 1000, tz=timezone.utc)
|
|
414
|
+
|
|
415
|
+
# Always start at the NEXT interval boundary
|
|
416
|
+
return start_time + timedelta(milliseconds=interval_ms)
|
|
417
|
+
|
|
418
|
+
def stop(self) -> None:
|
|
419
|
+
"""Stop the aggregator and cancel the timer."""
|
|
420
|
+
self._clock.cancel_timer(self._timer_name)
|
|
421
|
+
|
|
422
|
+
def _apply_update(self, trade: Trade) -> None:
|
|
423
|
+
"""
|
|
424
|
+
Apply trade update with time-based aggregation.
|
|
425
|
+
|
|
426
|
+
Parameters
|
|
427
|
+
----------
|
|
428
|
+
trade : Trade
|
|
429
|
+
The trade to process
|
|
430
|
+
"""
|
|
431
|
+
self._builder.update(trade)
|
|
432
|
+
|
|
433
|
+
def _build_bar(self, event: TimeEvent) -> None:
|
|
434
|
+
"""
|
|
435
|
+
Build and emit kline on timer event.
|
|
436
|
+
|
|
437
|
+
Parameters
|
|
438
|
+
----------
|
|
439
|
+
event : TimeEvent
|
|
440
|
+
The timer event
|
|
441
|
+
"""
|
|
442
|
+
if not self._build_with_no_updates and self._builder.count == 0:
|
|
443
|
+
return
|
|
444
|
+
ts_event = event.ts_event // 1_000_000 - self._interval_ms # Convert ns to ms
|
|
445
|
+
ts_init = event.ts_init // 1_000_000 # Convert ns to ms
|
|
446
|
+
|
|
447
|
+
# # Build and send kline (only if trades were received)
|
|
448
|
+
self._build_and_send(ts_event, ts_init)
|
|
449
|
+
self._log.debug(f"Kline built: ts_event={ts_event}, ts_init={ts_init}")
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Dict, Set, List, Optional, Type, Any, TypeVar
|
|
3
|
+
|
|
4
|
+
from walrasquant.schema import Order, Position, Balance, AccountBalance
|
|
5
|
+
from walrasquant.constants import AccountType, ExchangeType
|
|
6
|
+
|
|
7
|
+
_T = TypeVar("_T", Order, Position, Balance)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StorageBackend(ABC):
|
|
11
|
+
def __init__(
|
|
12
|
+
self, strategy_id: str, user_id: str, table_prefix: str, log, **kwargs
|
|
13
|
+
):
|
|
14
|
+
self.strategy_id = strategy_id
|
|
15
|
+
self.user_id = user_id
|
|
16
|
+
self.table_prefix = table_prefix
|
|
17
|
+
self._log = log
|
|
18
|
+
self._storage_initialized = False
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
async def _init_conn(self) -> None:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
async def _init_table(self) -> None:
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
async def close(self) -> None:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
async def sync_orders(self, mem_orders: Dict[str, Order]) -> None:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
async def sync_positions(self, mem_positions: Dict[str, Position]) -> None:
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
async def sync_open_orders(
|
|
42
|
+
self,
|
|
43
|
+
mem_open_orders: Dict[ExchangeType, Set[str]],
|
|
44
|
+
mem_orders: Dict[str, Order],
|
|
45
|
+
) -> None:
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
async def sync_balances(
|
|
50
|
+
self, mem_account_balance: Dict[AccountType, AccountBalance]
|
|
51
|
+
) -> None:
|
|
52
|
+
pass
|
|
53
|
+
|
|
54
|
+
@abstractmethod
|
|
55
|
+
def get_order(
|
|
56
|
+
self,
|
|
57
|
+
oid: str,
|
|
58
|
+
mem_orders: Dict[str, Order],
|
|
59
|
+
) -> Optional[Order]:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
@abstractmethod
|
|
63
|
+
def get_symbol_orders(self, symbol: str) -> Set[str]:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def get_all_positions(self, exchange_id: ExchangeType) -> Dict[str, Position]:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
@abstractmethod
|
|
71
|
+
def get_all_balances(self, account_type: AccountType) -> List[Balance]:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
@abstractmethod
|
|
75
|
+
async def sync_params(self, mem_params: Dict[str, Any]) -> None:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
@abstractmethod
|
|
79
|
+
def get_param(self, key: str, default: Any = None) -> Any:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
@abstractmethod
|
|
83
|
+
def get_all_params(self) -> Dict[str, Any]:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
async def start(self) -> None:
|
|
87
|
+
await self._init_conn()
|
|
88
|
+
await self._init_table()
|
|
89
|
+
self._storage_initialized = True
|
|
90
|
+
|
|
91
|
+
def _encode(self, obj: Order | Position | Balance) -> bytes:
|
|
92
|
+
import msgspec
|
|
93
|
+
|
|
94
|
+
return msgspec.json.encode(obj)
|
|
95
|
+
|
|
96
|
+
def _decode(self, data: bytes, obj_type: Type[_T]) -> _T:
|
|
97
|
+
import msgspec
|
|
98
|
+
|
|
99
|
+
return msgspec.json.decode(data, type=obj_type)
|
|
100
|
+
|
|
101
|
+
def _encode_param(self, obj: Any) -> bytes:
|
|
102
|
+
import msgspec
|
|
103
|
+
|
|
104
|
+
return msgspec.json.encode(obj)
|
|
105
|
+
|
|
106
|
+
def _decode_param(self, data: bytes) -> Any:
|
|
107
|
+
import msgspec
|
|
108
|
+
|
|
109
|
+
return msgspec.json.decode(data)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from typing import Dict, Set, List, Optional, Any
|
|
2
|
+
|
|
3
|
+
from walrasquant.backends.db import StorageBackend
|
|
4
|
+
from walrasquant.schema import Order, Position, Balance, AccountBalance
|
|
5
|
+
from walrasquant.constants import AccountType, ExchangeType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MemoryBackend(StorageBackend):
|
|
9
|
+
"""No-op backend for testing — all state lives in AsyncCache's own dicts."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, **kwargs):
|
|
12
|
+
# Accept and discard all kwargs (strategy_id, user_id, table_prefix, log, etc.)
|
|
13
|
+
self._storage_initialized = False
|
|
14
|
+
|
|
15
|
+
async def _init_conn(self) -> None:
|
|
16
|
+
pass
|
|
17
|
+
|
|
18
|
+
async def _init_table(self) -> None:
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
async def close(self) -> None:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
async def sync_orders(self, mem_orders: Dict[str, Order]) -> None:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
async def sync_positions(self, mem_positions: Dict[str, Position]) -> None:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
async def sync_open_orders(
|
|
31
|
+
self,
|
|
32
|
+
mem_open_orders: Dict[ExchangeType, Set[str]],
|
|
33
|
+
mem_orders: Dict[str, Order],
|
|
34
|
+
) -> None:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
async def sync_balances(
|
|
38
|
+
self, mem_account_balance: Dict[AccountType, AccountBalance]
|
|
39
|
+
) -> None:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
async def sync_params(self, mem_params: Dict[str, Any]) -> None:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
def get_order(self, oid: str, mem_orders: Dict[str, Order]) -> Optional[Order]:
|
|
46
|
+
return mem_orders.get(oid)
|
|
47
|
+
|
|
48
|
+
def get_symbol_orders(self, symbol: str) -> Set[str]:
|
|
49
|
+
return set()
|
|
50
|
+
|
|
51
|
+
def get_all_positions(self, exchange_id: ExchangeType) -> Dict[str, Position]:
|
|
52
|
+
return {}
|
|
53
|
+
|
|
54
|
+
def get_all_balances(self, account_type: AccountType) -> List[Balance]:
|
|
55
|
+
return []
|
|
56
|
+
|
|
57
|
+
def get_param(self, key: str, default: Any = None) -> Any:
|
|
58
|
+
return default
|
|
59
|
+
|
|
60
|
+
def get_all_params(self) -> Dict[str, Any]:
|
|
61
|
+
return {}
|