walrasquant-lib 0.4.20__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. walrasquant/__init__.py +7 -0
  2. walrasquant/aggregation.py +449 -0
  3. walrasquant/backends/__init__.py +5 -0
  4. walrasquant/backends/db.py +109 -0
  5. walrasquant/backends/db_memory.py +61 -0
  6. walrasquant/backends/db_postgresql.py +321 -0
  7. walrasquant/backends/db_sqlite.py +310 -0
  8. walrasquant/base/__init__.py +24 -0
  9. walrasquant/base/api_client.py +46 -0
  10. walrasquant/base/connector.py +863 -0
  11. walrasquant/base/ems.py +794 -0
  12. walrasquant/base/exchange.py +213 -0
  13. walrasquant/base/oms.py +428 -0
  14. walrasquant/base/retry.py +220 -0
  15. walrasquant/base/sms.py +545 -0
  16. walrasquant/base/ws_client.py +408 -0
  17. walrasquant/config.py +284 -0
  18. walrasquant/constants.py +413 -0
  19. walrasquant/core/__init__.py +0 -0
  20. walrasquant/core/cache.py +688 -0
  21. walrasquant/core/clock.py +59 -0
  22. walrasquant/core/connection.py +41 -0
  23. walrasquant/core/entity.py +504 -0
  24. walrasquant/core/nautilius_core.py +103 -0
  25. walrasquant/core/registry.py +41 -0
  26. walrasquant/engine.py +745 -0
  27. walrasquant/error.py +34 -0
  28. walrasquant/exchange/__init__.py +13 -0
  29. walrasquant/exchange/base_factory.py +172 -0
  30. walrasquant/exchange/binance/__init__.py +30 -0
  31. walrasquant/exchange/binance/connector.py +1093 -0
  32. walrasquant/exchange/binance/constants.py +934 -0
  33. walrasquant/exchange/binance/ems.py +140 -0
  34. walrasquant/exchange/binance/error.py +48 -0
  35. walrasquant/exchange/binance/exchange.py +144 -0
  36. walrasquant/exchange/binance/factory.py +115 -0
  37. walrasquant/exchange/binance/oms.py +1807 -0
  38. walrasquant/exchange/binance/rest_api.py +1653 -0
  39. walrasquant/exchange/binance/schema.py +1063 -0
  40. walrasquant/exchange/binance/websockets.py +389 -0
  41. walrasquant/exchange/bitget/__init__.py +28 -0
  42. walrasquant/exchange/bitget/connector.py +578 -0
  43. walrasquant/exchange/bitget/constants.py +392 -0
  44. walrasquant/exchange/bitget/ems.py +202 -0
  45. walrasquant/exchange/bitget/error.py +36 -0
  46. walrasquant/exchange/bitget/exchange.py +128 -0
  47. walrasquant/exchange/bitget/factory.py +135 -0
  48. walrasquant/exchange/bitget/oms.py +1619 -0
  49. walrasquant/exchange/bitget/rest_api.py +610 -0
  50. walrasquant/exchange/bitget/schema.py +885 -0
  51. walrasquant/exchange/bitget/websockets.py +753 -0
  52. walrasquant/exchange/bybit/__init__.py +32 -0
  53. walrasquant/exchange/bybit/connector.py +819 -0
  54. walrasquant/exchange/bybit/constants.py +479 -0
  55. walrasquant/exchange/bybit/ems.py +93 -0
  56. walrasquant/exchange/bybit/error.py +36 -0
  57. walrasquant/exchange/bybit/exchange.py +108 -0
  58. walrasquant/exchange/bybit/factory.py +128 -0
  59. walrasquant/exchange/bybit/oms.py +1195 -0
  60. walrasquant/exchange/bybit/rest_api.py +570 -0
  61. walrasquant/exchange/bybit/schema.py +867 -0
  62. walrasquant/exchange/bybit/websockets.py +307 -0
  63. walrasquant/exchange/hyperliquid/__init__.py +28 -0
  64. walrasquant/exchange/hyperliquid/connector.py +370 -0
  65. walrasquant/exchange/hyperliquid/constants.py +371 -0
  66. walrasquant/exchange/hyperliquid/ems.py +156 -0
  67. walrasquant/exchange/hyperliquid/error.py +48 -0
  68. walrasquant/exchange/hyperliquid/exchange.py +120 -0
  69. walrasquant/exchange/hyperliquid/factory.py +135 -0
  70. walrasquant/exchange/hyperliquid/oms.py +1081 -0
  71. walrasquant/exchange/hyperliquid/rest_api.py +348 -0
  72. walrasquant/exchange/hyperliquid/schema.py +583 -0
  73. walrasquant/exchange/hyperliquid/websockets.py +592 -0
  74. walrasquant/exchange/okx/__init__.py +25 -0
  75. walrasquant/exchange/okx/connector.py +931 -0
  76. walrasquant/exchange/okx/constants.py +518 -0
  77. walrasquant/exchange/okx/ems.py +144 -0
  78. walrasquant/exchange/okx/error.py +66 -0
  79. walrasquant/exchange/okx/exchange.py +102 -0
  80. walrasquant/exchange/okx/factory.py +138 -0
  81. walrasquant/exchange/okx/oms.py +1199 -0
  82. walrasquant/exchange/okx/rest_api.py +799 -0
  83. walrasquant/exchange/okx/schema.py +1449 -0
  84. walrasquant/exchange/okx/websockets.py +420 -0
  85. walrasquant/exchange/registry.py +201 -0
  86. walrasquant/execution/__init__.py +24 -0
  87. walrasquant/execution/algorithm.py +968 -0
  88. walrasquant/execution/algorithms/__init__.py +3 -0
  89. walrasquant/execution/algorithms/twap.py +392 -0
  90. walrasquant/execution/config.py +34 -0
  91. walrasquant/execution/constants.py +27 -0
  92. walrasquant/execution/schema.py +62 -0
  93. walrasquant/indicator.py +382 -0
  94. walrasquant/push.py +77 -0
  95. walrasquant/schema.py +755 -0
  96. walrasquant/strategy.py +1805 -0
  97. walrasquant/tools/__init__.py +0 -0
  98. walrasquant/tools/pm2_wrapper.py +1016 -0
  99. walrasquant/web/__init__.py +26 -0
  100. walrasquant/web/app.py +157 -0
  101. walrasquant/web/server.py +92 -0
  102. walrasquant_lib-0.4.20.dist-info/METADATA +162 -0
  103. walrasquant_lib-0.4.20.dist-info/RECORD +105 -0
  104. walrasquant_lib-0.4.20.dist-info/WHEEL +4 -0
  105. walrasquant_lib-0.4.20.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,7 @@
1
+ from importlib.metadata import version, PackageNotFoundError
2
+
3
+
4
+ try:
5
+ __version__ = version("walrasquant-lib")
6
+ except PackageNotFoundError:
7
+ __version__ = "unknown"
@@ -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,5 @@
1
+ from walrasquant.backends.db_sqlite import SQLiteBackend
2
+ from walrasquant.backends.db_postgresql import PostgreSQLBackend
3
+ from walrasquant.backends.db_memory import MemoryBackend
4
+
5
+ __all__ = ["SQLiteBackend", "PostgreSQLBackend", "MemoryBackend"]
@@ -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 {}