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,382 @@
1
+ import numpy as np
2
+ from collections import defaultdict
3
+ from typing import Dict
4
+ import re
5
+ from walrasquant.schema import (
6
+ BookL1,
7
+ BookL2,
8
+ Kline,
9
+ Trade,
10
+ IndexPrice,
11
+ FundingRate,
12
+ MarkPrice,
13
+ )
14
+ from walrasquant.core.nautilius_core import MessageBus
15
+ from walrasquant.constants import KlineInterval
16
+
17
+
18
+ def _validate_indicator_name(name: str) -> bool:
19
+ """
20
+ Validate indicator name for use with getattr access pattern.
21
+
22
+ Rules:
23
+ - Must be a valid Python identifier
24
+ - Cannot start with underscore (reserved for private attributes)
25
+ - Cannot be a Python keyword
26
+ - Must contain only alphanumeric characters and underscores
27
+ - Must start with a letter or underscore (but not underscore per rule above)
28
+
29
+ Args:
30
+ name: The indicator name to validate
31
+
32
+ Returns:
33
+ bool: True if name is valid, False otherwise
34
+ """
35
+ import keyword
36
+
37
+ if not name:
38
+ return False
39
+
40
+ # Check if it's a valid Python identifier
41
+ if not name.isidentifier():
42
+ return False
43
+
44
+ # Cannot start with underscore (reserved for internal use)
45
+ if name.startswith("_"):
46
+ return False
47
+
48
+ # Cannot be a Python keyword
49
+ if keyword.iskeyword(name):
50
+ return False
51
+
52
+ # Additional check: must match pattern for safe attribute access
53
+ if not re.match(r"^[a-zA-Z][a-zA-Z0-9_]*$", name):
54
+ return False
55
+
56
+ return True
57
+
58
+
59
+ class Indicator:
60
+ def __init__(
61
+ self,
62
+ params: dict | None = None,
63
+ name: str | None = None,
64
+ warmup_period: int | None = None,
65
+ kline_interval: KlineInterval | None = None,
66
+ warmup_include_unclosed: bool = False,
67
+ ):
68
+ indicator_name = name or type(self).__name__
69
+
70
+ # Validate the indicator name for getattr access
71
+ if not _validate_indicator_name(indicator_name):
72
+ raise ValueError(
73
+ f"Invalid indicator name '{indicator_name}'. "
74
+ f"Name must be a valid Python identifier, cannot start with underscore, "
75
+ f"cannot be a Python keyword, and must contain only alphanumeric characters and underscores."
76
+ )
77
+
78
+ if kline_interval is not None and not isinstance(kline_interval, KlineInterval):
79
+ raise ValueError("kline_interval must be a single KlineInterval")
80
+
81
+ self.name = indicator_name
82
+ self.params = params
83
+ self.warmup_period = warmup_period
84
+ self.kline_interval = kline_interval
85
+ self.warmup_include_unclosed = warmup_include_unclosed
86
+ self._is_warmed_up = False
87
+ self._warmup_data_count = 0
88
+
89
+ def handle_bookl1(self, bookl1: BookL1):
90
+ raise NotImplementedError
91
+
92
+ def handle_bookl2(self, bookl2: BookL2):
93
+ raise NotImplementedError
94
+
95
+ def handle_kline(self, kline: Kline):
96
+ raise NotImplementedError
97
+
98
+ def handle_trade(self, trade: Trade):
99
+ raise NotImplementedError
100
+
101
+ def handle_index_price(self, index_price: IndexPrice):
102
+ """Handle index price updates if applicable."""
103
+ raise NotImplementedError
104
+
105
+ def handle_funding_rate(self, funding_rate: FundingRate):
106
+ """Handle funding rate updates if applicable."""
107
+ raise NotImplementedError
108
+
109
+ def handle_mark_price(self, mark_price: MarkPrice):
110
+ """Handle mark price updates if applicable."""
111
+ raise NotImplementedError
112
+
113
+ @property
114
+ def requires_warmup(self) -> bool:
115
+ """Check if this indicator requires warmup."""
116
+ return self.warmup_period is not None and self.kline_interval is not None
117
+
118
+ @property
119
+ def is_warmed_up(self) -> bool:
120
+ """Check if the indicator has completed its warmup period."""
121
+ if not self.requires_warmup:
122
+ return True
123
+ return self._is_warmed_up
124
+
125
+ def _process_warmup_kline(self, kline: Kline):
126
+ """Process a kline during warmup period."""
127
+ if not self.requires_warmup or self._is_warmed_up:
128
+ return
129
+
130
+ self._warmup_data_count += 1
131
+ self.handle_kline(kline)
132
+
133
+ if (
134
+ self.warmup_period is not None
135
+ and self._warmup_data_count >= self.warmup_period
136
+ ):
137
+ self._is_warmed_up = True
138
+
139
+ def reset_warmup(self):
140
+ """Reset warmup state. Useful for backtesting or restarting indicators."""
141
+ self._is_warmed_up = False
142
+ self._warmup_data_count = 0
143
+
144
+ @property
145
+ def value(self):
146
+ """Get the current value of the indicator."""
147
+ raise NotImplementedError(
148
+ "Subclasses must implement the 'value' property to return the indicator's value."
149
+ )
150
+
151
+
152
+ class IndicatorManager:
153
+ def __init__(self, msgbus: MessageBus):
154
+ self._bookl1_indicators: dict[str, list[Indicator]] = defaultdict(list)
155
+ self._bookl2_indicators: dict[str, list[Indicator]] = defaultdict(list)
156
+ self._kline_indicators: dict[tuple[str, KlineInterval], list[Indicator]] = (
157
+ defaultdict(list)
158
+ )
159
+ self._trade_indicators: dict[str, list[Indicator]] = defaultdict(list)
160
+ self._index_price_indicators: dict[str, list[Indicator]] = defaultdict(list)
161
+ self._funding_rate_indicators: dict[str, list[Indicator]] = defaultdict(list)
162
+ self._mark_price_indicators: dict[str, list[Indicator]] = defaultdict(list)
163
+ self._warmup_pending: dict[tuple[str, KlineInterval], list[Indicator]] = (
164
+ defaultdict(list)
165
+ )
166
+
167
+ msgbus.subscribe(topic="bookl1", handler=self.on_bookl1)
168
+ msgbus.subscribe(topic="bookl2", handler=self.on_bookl2)
169
+ msgbus.subscribe(topic="kline", handler=self.on_kline)
170
+ msgbus.subscribe(topic="trade", handler=self.on_trade)
171
+ msgbus.subscribe(topic="index_price", handler=self.on_index_price)
172
+ msgbus.subscribe(topic="funding_rate", handler=self.on_funding_rate)
173
+ msgbus.subscribe(topic="mark_price", handler=self.on_mark_price)
174
+
175
+ def add_bookl1_indicator(self, symbol: str, indicator: Indicator):
176
+ self._bookl1_indicators[symbol].append(indicator)
177
+
178
+ def add_bookl2_indicator(self, symbol: str, indicator: Indicator):
179
+ self._bookl2_indicators[symbol].append(indicator)
180
+
181
+ def add_kline_indicator(self, symbol: str, indicator: Indicator):
182
+ if indicator.kline_interval is None:
183
+ raise ValueError(
184
+ f"Indicator {indicator.name} requires kline_interval to be set for kline data processing"
185
+ )
186
+
187
+ key = (symbol, indicator.kline_interval)
188
+ if indicator.requires_warmup:
189
+ self._warmup_pending[key].append(indicator)
190
+ else:
191
+ self._kline_indicators[key].append(indicator)
192
+
193
+ def add_trade_indicator(self, symbol: str, indicator: Indicator):
194
+ self._trade_indicators[symbol].append(indicator)
195
+
196
+ def add_index_price_indicator(self, symbol: str, indicator: Indicator):
197
+ self._index_price_indicators[symbol].append(indicator)
198
+
199
+ def add_funding_rate_indicator(self, symbol: str, indicator: Indicator):
200
+ self._funding_rate_indicators[symbol].append(indicator)
201
+
202
+ def add_mark_price_indicator(self, symbol: str, indicator: Indicator):
203
+ self._mark_price_indicators[symbol].append(indicator)
204
+
205
+ def on_bookl1(self, bookl1: BookL1):
206
+ symbol = bookl1.symbol
207
+ for indicator in self._bookl1_indicators[symbol]:
208
+ indicator.handle_bookl1(bookl1)
209
+
210
+ def on_bookl2(self, bookl2: BookL2):
211
+ symbol = bookl2.symbol
212
+ for indicator in self._bookl2_indicators[symbol]:
213
+ indicator.handle_bookl2(bookl2)
214
+
215
+ def on_kline(self, kline: Kline):
216
+ key = (kline.symbol, kline.interval)
217
+
218
+ # Process active indicators for this symbol+interval
219
+ for indicator in self._kline_indicators[key]:
220
+ indicator.handle_kline(kline)
221
+
222
+ # Process warmup indicators and check if they're ready
223
+ warmup_indicators = self._warmup_pending[key][:]
224
+ for indicator in warmup_indicators:
225
+ indicator._process_warmup_kline(kline)
226
+ if indicator.is_warmed_up:
227
+ self._warmup_pending[key].remove(indicator)
228
+ self._kline_indicators[key].append(indicator)
229
+
230
+ def on_trade(self, trade: Trade):
231
+ symbol = trade.symbol
232
+ for indicator in self._trade_indicators[symbol]:
233
+ indicator.handle_trade(trade)
234
+
235
+ def on_index_price(self, index_price: IndexPrice):
236
+ symbol = index_price.symbol
237
+ for indicator in self._index_price_indicators[symbol]:
238
+ indicator.handle_index_price(index_price)
239
+
240
+ def on_funding_rate(self, funding_rate: FundingRate):
241
+ symbol = funding_rate.symbol
242
+ for indicator in self._funding_rate_indicators[symbol]:
243
+ indicator.handle_funding_rate(funding_rate)
244
+
245
+ def on_mark_price(self, mark_price: MarkPrice):
246
+ symbol = mark_price.symbol
247
+ for indicator in self._mark_price_indicators[symbol]:
248
+ indicator.handle_mark_price(mark_price)
249
+
250
+ @property
251
+ def bookl1_subscribed_symbols(self):
252
+ return list(self._bookl1_indicators.keys())
253
+
254
+ @property
255
+ def bookl2_subscribed_symbols(self):
256
+ return list(self._bookl2_indicators.keys())
257
+
258
+ @property
259
+ def kline_subscribed_symbols(self):
260
+ return list(set(key[0] for key in self._kline_indicators.keys()))
261
+
262
+ @property
263
+ def trade_subscribed_symbols(self):
264
+ return list(self._trade_indicators.keys())
265
+
266
+ @property
267
+ def index_price_subscribed_symbols(self):
268
+ return list(self._index_price_indicators.keys())
269
+
270
+ @property
271
+ def funding_rate_subscribed_symbols(self):
272
+ return list(self._funding_rate_indicators.keys())
273
+
274
+ @property
275
+ def mark_price_subscribed_symbols(self):
276
+ return list(self._mark_price_indicators.keys())
277
+
278
+ def get_warmup_requirements(
279
+ self,
280
+ ) -> dict[tuple[str, KlineInterval], list[tuple[Indicator, int, KlineInterval]]]:
281
+ """Get warmup requirements for all pending indicators by symbol and interval."""
282
+ requirements = defaultdict(list)
283
+ for key, indicators in self._warmup_pending.items():
284
+ for indicator in indicators:
285
+ if indicator.requires_warmup:
286
+ requirements[key].append(
287
+ (indicator, indicator.warmup_period, indicator.kline_interval)
288
+ )
289
+ return dict(requirements)
290
+
291
+ def has_warmup_pending(
292
+ self, symbol: str | None = None, interval: KlineInterval | None = None
293
+ ) -> bool:
294
+ """Check if there are indicators pending warmup."""
295
+ if symbol and interval:
296
+ key = (symbol, interval)
297
+ return len(self._warmup_pending[key]) > 0
298
+ elif symbol:
299
+ return any(
300
+ len(indicators) > 0
301
+ for key, indicators in self._warmup_pending.items()
302
+ if key[0] == symbol
303
+ )
304
+ return any(len(indicators) > 0 for indicators in self._warmup_pending.values())
305
+
306
+ def warmup_pending_symbols(self) -> list[str]:
307
+ """Get list of symbols with indicators pending warmup."""
308
+ return list(
309
+ set(
310
+ key[0] for key, indicators in self._warmup_pending.items() if indicators
311
+ )
312
+ )
313
+
314
+
315
+ class IndicatorContainer:
316
+ """Container for accessing indicators by symbol."""
317
+
318
+ def __init__(self):
319
+ self._indicators: Dict[str, Indicator] = {}
320
+
321
+ def __getitem__(self, symbol: str):
322
+ """Get indicator for specific symbol."""
323
+ return self._indicators.get(symbol)
324
+
325
+ def __setitem__(self, symbol: str, indicator: Indicator):
326
+ """Set indicator for specific symbol."""
327
+ self._indicators[symbol] = indicator
328
+
329
+ def __contains__(self, symbol: str):
330
+ """Check if symbol has an indicator."""
331
+ return symbol in self._indicators
332
+
333
+ def get(self, symbol: str, default=None):
334
+ """Get indicator for symbol with default value."""
335
+ return self._indicators.get(symbol, default)
336
+
337
+ def symbols(self):
338
+ """Get all symbols with indicators."""
339
+ return list(self._indicators.keys())
340
+
341
+
342
+ class IndicatorProxy:
343
+ """Proxy for accessing per-symbol indicator instances."""
344
+
345
+ def __init__(self):
346
+ self._containers: Dict[str, IndicatorContainer] = {}
347
+
348
+ def __getattr__(self, name):
349
+ """Get indicator container by name."""
350
+ if name not in self._containers:
351
+ self._containers[name] = IndicatorContainer()
352
+ return self._containers[name]
353
+
354
+ def register_indicator(self, name: str, symbol: str, indicator: Indicator):
355
+ """Register an indicator instance for a specific symbol."""
356
+ container = getattr(self, name)
357
+ container[symbol] = indicator
358
+
359
+
360
+ class RingBuffer:
361
+ def __init__(self, length: int):
362
+ self._length = length
363
+ self._buffer = np.zeros(length, dtype=np.float64)
364
+ self._idx = 0
365
+ self.is_full = False
366
+
367
+ def append(self, val: float):
368
+ self._buffer[self._idx] = val
369
+ self._idx = (self._idx + 1) % self._length
370
+ if not self.is_full and self._idx == 0:
371
+ self.is_full = True
372
+
373
+ def get_as_numpy_array(self) -> np.ndarray:
374
+ if not self.is_full:
375
+ return self._buffer[: self._idx].copy()
376
+ indexes = np.arange(self._idx, self._idx + self._length) % self._length
377
+ return self._buffer[indexes]
378
+
379
+ def get_last_value(self) -> float:
380
+ if self._idx == 0 and not self.is_full:
381
+ return np.nan
382
+ return self._buffer[self._idx - 1]
walrasquant/push.py ADDED
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Literal, Optional
5
+ from flashduty_sdk import FlashDutyClient
6
+
7
+ import nexuslog as logging
8
+
9
+
10
+ EventStatus = Literal["Ok", "Info", "Warning", "Critical"]
11
+
12
+
13
+ class FlashDutyConfigError(RuntimeError):
14
+ pass
15
+
16
+
17
+ @dataclass
18
+ class FlashDutyPushService:
19
+ integration_key: str | None
20
+ strategy_id: str | None
21
+ user_id: str | None
22
+
23
+ def __post_init__(self) -> None:
24
+ self._log = logging.getLogger(name=type(self).__name__)
25
+ self._client: FlashDutyClient | None = None
26
+
27
+ if not self.integration_key:
28
+ return
29
+
30
+ self._client = FlashDutyClient(self.integration_key)
31
+
32
+ def _ensure_ready(self) -> None:
33
+ if not self.integration_key:
34
+ raise FlashDutyConfigError(
35
+ "FlashDuty integration_key is not configured in Config"
36
+ )
37
+ if self._client is None:
38
+ raise FlashDutyConfigError("FlashDuty client is not initialized")
39
+
40
+ def send_alert(
41
+ self,
42
+ event_status: EventStatus,
43
+ title_rule: str,
44
+ alert_key: Optional[str] = None,
45
+ description: Optional[str] = None,
46
+ labels: Optional[dict[str, str]] = None,
47
+ images: Optional[list[dict[str, str]]] = None,
48
+ ) -> None:
49
+ self._ensure_ready()
50
+ client = self._client
51
+ if client is None:
52
+ raise FlashDutyConfigError("FlashDuty client is not initialized")
53
+
54
+ merged_labels: dict[str, str] = {}
55
+ if labels:
56
+ merged_labels.update(labels)
57
+ if self.strategy_id is not None:
58
+ merged_labels["strategy_id"] = self.strategy_id
59
+ if self.user_id is not None:
60
+ merged_labels["user_id"] = self.user_id
61
+
62
+ client.send_alert(
63
+ event_status=event_status,
64
+ title_rule=title_rule,
65
+ alert_key=alert_key,
66
+ description=description,
67
+ labels=merged_labels or None,
68
+ images=images,
69
+ )
70
+
71
+ def shutdown(self) -> None:
72
+ if self._client is None:
73
+ return
74
+ try:
75
+ self._client.shutdown()
76
+ except Exception as exc: # pragma: no cover - defensive shutdown
77
+ self._log.debug(f"FlashDuty shutdown failed: {exc}")