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/indicator.py
ADDED
|
@@ -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}")
|