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,1805 @@
1
+ import asyncio
2
+ import inspect
3
+ import os
4
+ import signal
5
+ import copy
6
+ from datetime import datetime, timedelta
7
+ from typing import Dict, List, Callable, Literal, Optional, Any
8
+ from decimal import Decimal
9
+ from apscheduler.schedulers.asyncio import AsyncIOScheduler
10
+ import nexuslog as logging
11
+ from walrasquant.base import ExchangeManager
12
+ from walrasquant.indicator import IndicatorManager, Indicator, IndicatorProxy
13
+ from walrasquant.core.clock import precise_sleep
14
+ from walrasquant.core.entity import TaskManager
15
+ from walrasquant.core.cache import AsyncCache
16
+ from walrasquant.error import StrategyBuildError
17
+ from walrasquant.base import (
18
+ ExecutionManagementSystem,
19
+ PrivateConnector,
20
+ PublicConnector,
21
+ SubscriptionManagementSystem,
22
+ )
23
+ from walrasquant.core.entity import OidGen
24
+ from walrasquant.core.nautilius_core import MessageBus, LiveClock
25
+ from walrasquant.schema import (
26
+ BookL1,
27
+ Trade,
28
+ Kline,
29
+ BookL2,
30
+ Order,
31
+ FundingRate,
32
+ Ticker,
33
+ IndexPrice,
34
+ MarkPrice,
35
+ InstrumentId,
36
+ BaseMarket,
37
+ AccountBalance,
38
+ CreateOrderSubmit,
39
+ TakeProfitAndStopLossOrderSubmit,
40
+ # TWAPOrderSubmit,
41
+ ModifyOrderSubmit,
42
+ CancelOrderSubmit,
43
+ CancelAllOrderSubmit,
44
+ # CancelTWAPOrderSubmit,
45
+ KlineList,
46
+ BatchOrder,
47
+ BatchOrderSubmit,
48
+ CancelBatchOrder,
49
+ )
50
+ from walrasquant.constants import (
51
+ DataType,
52
+ BookLevel,
53
+ OrderSide,
54
+ OrderType,
55
+ TimeInForce,
56
+ ParamBackend,
57
+ # PositionSide,
58
+ AccountType,
59
+ SubmitType,
60
+ ExchangeType,
61
+ KlineInterval,
62
+ TriggerType,
63
+ BACKEND_LITERAL,
64
+ )
65
+ from walrasquant.core.connection import ConnectionPolicyState
66
+ from walrasquant.push import FlashDutyPushService, EventStatus
67
+ from walrasquant.execution import (
68
+ ExecAlgorithm,
69
+ ExecAlgorithmCommand,
70
+ ExecAlgorithmCommandType,
71
+ ExecAlgorithmOrder,
72
+ ExecAlgorithmOrderParams,
73
+ )
74
+
75
+
76
+ class Strategy:
77
+ def __init_subclass__(cls, **kwargs):
78
+ super().__init_subclass__(**kwargs)
79
+ async_methods = [
80
+ name
81
+ for name, method in cls.__dict__.items()
82
+ if inspect.iscoroutinefunction(method)
83
+ ]
84
+ if async_methods:
85
+ raise TypeError(
86
+ f"{cls.__name__} defines async methods which are not allowed in Strategy: "
87
+ + ", ".join(async_methods)
88
+ )
89
+
90
+ def __init__(self):
91
+ # Track which symbols use aggregator: {(interval, symbol): use_aggregator}
92
+ self._kline_use_aggregator: list = []
93
+
94
+ self._initialized = False
95
+ self._started = False
96
+ self._scheduler = AsyncIOScheduler()
97
+ # On-loop fast timers (schedule(..., fast=True)), tracked for cancellation
98
+ # at dispose. Each entry is the per-timer state dict from _schedule_fast.
99
+ self._fast_timers: list[dict] = []
100
+ self.indicator = IndicatorProxy()
101
+ self._connection_status: ConnectionPolicyState | None = None
102
+
103
+ def register(
104
+ self,
105
+ exchanges: Dict[ExchangeType, ExchangeManager],
106
+ public_connectors: Dict[AccountType, PublicConnector],
107
+ private_connectors: Dict[AccountType, PrivateConnector],
108
+ cache: AsyncCache,
109
+ msgbus: MessageBus,
110
+ clock: LiveClock,
111
+ oidgen: OidGen,
112
+ task_manager: TaskManager,
113
+ sms: SubscriptionManagementSystem,
114
+ ems: Dict[ExchangeType, ExecutionManagementSystem],
115
+ push_service: FlashDutyPushService,
116
+ exec_algorithms: Dict[str, ExecAlgorithm] | None = None,
117
+ strategy_id: str | None = None,
118
+ user_id: str | None = None,
119
+ ) -> None:
120
+ if self._initialized:
121
+ return
122
+
123
+ self.log = logging.getLogger(name=type(self).__name__)
124
+ self._sys_log = logging.getLogger(name=Strategy.__name__)
125
+
126
+ self.cache = cache
127
+ self.clock = clock
128
+ self._oidgen = oidgen
129
+ self._ems = ems
130
+ self._sms = sms
131
+ self._task_manager = task_manager
132
+ self._msgbus = msgbus
133
+ self._private_connectors = private_connectors
134
+ self._public_connectors = public_connectors
135
+ self._exchanges = exchanges
136
+ self._indicator_manager = IndicatorManager(self._msgbus)
137
+ self._push_service = push_service
138
+ self._exec_algorithms = exec_algorithms if exec_algorithms is not None else {}
139
+
140
+ self._msgbus.register(endpoint="pending", handler=self.on_pending_order)
141
+ self._msgbus.register(endpoint="accepted", handler=self.on_accepted_order)
142
+ self._msgbus.register(
143
+ endpoint="partially_filled", handler=self.on_partially_filled_order
144
+ )
145
+ self._msgbus.register(endpoint="filled", handler=self.on_filled_order)
146
+ self._msgbus.register(endpoint="canceling", handler=self.on_canceling_order)
147
+ self._msgbus.register(endpoint="canceled", handler=self.on_canceled_order)
148
+ self._msgbus.register(endpoint="failed", handler=self.on_failed_order)
149
+ self._msgbus.register(
150
+ endpoint="cancel_failed", handler=self.on_cancel_failed_order
151
+ )
152
+ self._msgbus.register(endpoint="expired", handler=self.on_expired_order)
153
+
154
+ self._msgbus.register(endpoint="balance", handler=self.on_balance)
155
+ self._msgbus.register(
156
+ endpoint="connection_status", handler=self._handle_connection_status
157
+ )
158
+
159
+ self._initialized = True
160
+
161
+ @property
162
+ def ready(self):
163
+ return self._sms.ready
164
+
165
+ def send_alert(
166
+ self,
167
+ event_status: EventStatus,
168
+ title_rule: str,
169
+ alert_key: Optional[str] = None,
170
+ description: Optional[str] = None,
171
+ labels: Optional[dict[str, str]] = None,
172
+ images: Optional[list[dict[str, str]]] = None,
173
+ ) -> None:
174
+ self._push_service.send_alert(
175
+ event_status=event_status,
176
+ title_rule=title_rule,
177
+ alert_key=alert_key,
178
+ description=description,
179
+ labels=labels,
180
+ images=images,
181
+ )
182
+
183
+ def alert_ok(
184
+ self,
185
+ title_rule: str,
186
+ alert_key: Optional[str] = None,
187
+ description: Optional[str] = None,
188
+ labels: Optional[dict[str, str]] = None,
189
+ images: Optional[list[dict[str, str]]] = None,
190
+ ) -> None:
191
+ self.send_alert(
192
+ event_status="Ok",
193
+ title_rule=title_rule,
194
+ alert_key=alert_key,
195
+ description=description,
196
+ labels=labels,
197
+ images=images,
198
+ )
199
+
200
+ def alert_info(
201
+ self,
202
+ title_rule: str,
203
+ alert_key: Optional[str] = None,
204
+ description: Optional[str] = None,
205
+ labels: Optional[dict[str, str]] = None,
206
+ images: Optional[list[dict[str, str]]] = None,
207
+ ) -> None:
208
+ self.send_alert(
209
+ event_status="Info",
210
+ title_rule=title_rule,
211
+ alert_key=alert_key,
212
+ description=description,
213
+ labels=labels,
214
+ images=images,
215
+ )
216
+
217
+ def alert_warning(
218
+ self,
219
+ title_rule: str,
220
+ alert_key: Optional[str] = None,
221
+ description: Optional[str] = None,
222
+ labels: Optional[dict[str, str]] = None,
223
+ images: Optional[list[dict[str, str]]] = None,
224
+ ) -> None:
225
+ self.send_alert(
226
+ event_status="Warning",
227
+ title_rule=title_rule,
228
+ alert_key=alert_key,
229
+ description=description,
230
+ labels=labels,
231
+ images=images,
232
+ )
233
+
234
+ def alert_critical(
235
+ self,
236
+ title_rule: str,
237
+ alert_key: Optional[str] = None,
238
+ description: Optional[str] = None,
239
+ labels: Optional[dict[str, str]] = None,
240
+ images: Optional[list[dict[str, str]]] = None,
241
+ ) -> None:
242
+ self.send_alert(
243
+ event_status="Critical",
244
+ title_rule=title_rule,
245
+ alert_key=alert_key,
246
+ description=description,
247
+ labels=labels,
248
+ images=images,
249
+ )
250
+
251
+ @property
252
+ def connection_status(self) -> ConnectionPolicyState | None:
253
+ return self._connection_status
254
+
255
+ @property
256
+ def can_open(self) -> bool:
257
+ if self._connection_status is None:
258
+ return False
259
+ # allow_open == md_ok and td_ok
260
+ return self._connection_status.allow_open
261
+
262
+ @property
263
+ def can_trade(self) -> bool:
264
+ if self._connection_status is None:
265
+ return False
266
+ # allow_trade == td_ok
267
+ return self._connection_status.allow_trade
268
+
269
+ @property
270
+ def close_only(self) -> bool:
271
+ if self._connection_status is None:
272
+ return False
273
+ return self._connection_status.allow_close_only
274
+
275
+ def _handle_connection_status(self, status: ConnectionPolicyState) -> None:
276
+ """Internal handler for connection policy updates from the engine."""
277
+ self.on_connection_status(status)
278
+ self._connection_status = status
279
+
280
+ def on_connection_status(self, status: ConnectionPolicyState) -> None:
281
+ """Optional user hook for connection policy updates."""
282
+ pass
283
+
284
+ def tick_sz(self, symbol: str) -> float:
285
+ return self.market(symbol).precision.price
286
+
287
+ def lot_sz(self, symbol: str) -> float:
288
+ return self.market(symbol).precision.amount
289
+
290
+ def api(self, account_type: AccountType):
291
+ return self._private_connectors[account_type].api
292
+
293
+ def api_fire(self, account_type: AccountType):
294
+ """Fire-and-forget REST API call: non-blocking, no return value."""
295
+ return self._private_connectors[account_type].api_fire
296
+
297
+ def register_indicator(
298
+ self,
299
+ symbols: str | List[str],
300
+ indicator: Indicator,
301
+ data_type: DataType,
302
+ account_type: AccountType | None = None,
303
+ ):
304
+ if not self._initialized:
305
+ raise StrategyBuildError(
306
+ "Strategy not initialized, please use `register_indicator` in `on_start` method"
307
+ )
308
+
309
+ if isinstance(symbols, str):
310
+ symbols = [symbols]
311
+
312
+ # Create separate indicator instances for each symbol to avoid shared state
313
+ for symbol in symbols:
314
+ # Create a deep copy of the indicator for each symbol
315
+ symbol_indicator = copy.deepcopy(indicator)
316
+
317
+ # Register the symbol-specific indicator with the proxy
318
+ self.indicator.register_indicator(indicator.name, symbol, symbol_indicator)
319
+
320
+ match data_type:
321
+ case DataType.BOOKL1:
322
+ self._indicator_manager.add_bookl1_indicator(
323
+ symbol, symbol_indicator
324
+ )
325
+ case DataType.BOOKL2:
326
+ self._indicator_manager.add_bookl2_indicator(
327
+ symbol, symbol_indicator
328
+ )
329
+ case DataType.KLINE:
330
+ self._indicator_manager.add_kline_indicator(
331
+ symbol, symbol_indicator
332
+ )
333
+ case DataType.TRADE:
334
+ self._indicator_manager.add_trade_indicator(
335
+ symbol, symbol_indicator
336
+ )
337
+ case DataType.INDEX_PRICE:
338
+ self._indicator_manager.add_index_price_indicator(
339
+ symbol, symbol_indicator
340
+ )
341
+ case DataType.FUNDING_RATE:
342
+ self._indicator_manager.add_funding_rate_indicator(
343
+ symbol, symbol_indicator
344
+ )
345
+ case DataType.MARK_PRICE:
346
+ self._indicator_manager.add_mark_price_indicator(
347
+ symbol, symbol_indicator
348
+ )
349
+ case _:
350
+ raise ValueError(f"Invalid data type: {data_type}")
351
+
352
+ if symbol_indicator.requires_warmup:
353
+ warmup_account_type = account_type or self._infer_account_type(symbol)
354
+ self._perform_indicator_warmup(
355
+ symbol, symbol_indicator, warmup_account_type
356
+ )
357
+
358
+ def _infer_account_type(self, symbol: str) -> AccountType:
359
+ """
360
+ Infer the account type based on the symbol's exchange and type.
361
+ This is useful for methods that require an account type but don't have it explicitly provided.
362
+ """
363
+ instrument_id = InstrumentId.from_str(symbol)
364
+ exchange = self._exchanges.get(instrument_id.exchange)
365
+ if not exchange:
366
+ raise ValueError(
367
+ f"Exchange {instrument_id.exchange} not found, please add it to the config"
368
+ )
369
+ return exchange.instrument_id_to_account_type(instrument_id)
370
+
371
+ def request_ticker(
372
+ self,
373
+ symbol: str,
374
+ account_type: AccountType | None = None,
375
+ ) -> Ticker:
376
+ account_type = account_type or self._infer_account_type(symbol)
377
+ connector = self._public_connectors.get(account_type)
378
+ if not connector:
379
+ raise ValueError(
380
+ f"Account type {account_type} not found in public connectors"
381
+ )
382
+ return self._task_manager.run_sync(connector.request_ticker(symbol))
383
+
384
+ def request_all_tickers(
385
+ self,
386
+ account_type: AccountType,
387
+ ) -> Dict[str, Ticker]:
388
+ connector = self._public_connectors.get(account_type)
389
+ if not connector:
390
+ raise ValueError(
391
+ f"Account type {account_type} not found in public connectors"
392
+ )
393
+ return self._task_manager.run_sync(connector.request_all_tickers())
394
+
395
+ def request_klines(
396
+ self,
397
+ symbol: str | List[str],
398
+ interval: KlineInterval,
399
+ limit: int | None = None,
400
+ start_time: int | datetime | None = None,
401
+ end_time: int | datetime | None = None,
402
+ account_type: AccountType | None = None,
403
+ ) -> KlineList:
404
+ if isinstance(start_time, datetime):
405
+ start_time = int(start_time.timestamp() * 1000)
406
+ if isinstance(end_time, datetime):
407
+ end_time = int(end_time.timestamp() * 1000)
408
+
409
+ if isinstance(symbol, str):
410
+ symbol = [symbol]
411
+ inferred_symbol = symbol[0]
412
+ account_type = account_type or self._infer_account_type(inferred_symbol)
413
+ connector = self._public_connectors.get(account_type)
414
+ if not connector:
415
+ raise ValueError(
416
+ f"Account type {account_type} not found in public connectors"
417
+ )
418
+
419
+ klines = KlineList([])
420
+ for sym in symbol:
421
+ res = self._task_manager.run_sync(
422
+ connector.request_klines(
423
+ symbol=sym,
424
+ interval=interval,
425
+ limit=limit,
426
+ start_time=start_time,
427
+ end_time=end_time,
428
+ )
429
+ )
430
+ klines.extend(res)
431
+ return klines
432
+
433
+ def request_index_klines(
434
+ self,
435
+ symbol: str | List[str],
436
+ interval: KlineInterval,
437
+ limit: int | None = None,
438
+ start_time: int | datetime | None = None,
439
+ end_time: int | datetime | None = None,
440
+ account_type: AccountType | None = None,
441
+ ) -> KlineList:
442
+ if isinstance(start_time, datetime):
443
+ start_time = int(start_time.timestamp() * 1000)
444
+ if isinstance(end_time, datetime):
445
+ end_time = int(end_time.timestamp() * 1000)
446
+ if isinstance(symbol, str):
447
+ symbol = [symbol]
448
+ inferred_symbol = symbol[0]
449
+ account_type = account_type or self._infer_account_type(inferred_symbol)
450
+ connector = self._public_connectors.get(account_type)
451
+ if not connector:
452
+ raise ValueError(
453
+ f"Account type {account_type} not found in public connectors"
454
+ )
455
+
456
+ klines = KlineList([])
457
+ for sym in symbol:
458
+ res = self._task_manager.run_sync(
459
+ connector.request_index_klines(
460
+ symbol=sym,
461
+ interval=interval,
462
+ limit=limit,
463
+ start_time=start_time,
464
+ end_time=end_time,
465
+ )
466
+ )
467
+ klines.extend(res)
468
+ return klines
469
+
470
+ def _perform_indicator_warmup(
471
+ self, symbol: str, indicator: Indicator, account_type: AccountType
472
+ ):
473
+ """Automatically fetch historical data to warm up an indicator."""
474
+ try:
475
+ # Calculate how much historical data we need
476
+ if indicator.warmup_period is None:
477
+ raise ValueError("indicator.warmup_period is required for warmup")
478
+ if indicator.kline_interval is None:
479
+ raise ValueError("indicator.kline_interval is required for warmup")
480
+ warmup_limit = indicator.warmup_period
481
+ if not indicator.warmup_include_unclosed:
482
+ warmup_limit += 1
483
+ warmup_milliseconds = warmup_limit * indicator.kline_interval.milliseconds
484
+ start_time_ms = self.clock.timestamp_ms() - warmup_milliseconds
485
+
486
+ # Fetch historical klines
487
+ historical_klines = self.request_klines(
488
+ symbol=symbol,
489
+ account_type=account_type,
490
+ interval=indicator.kline_interval,
491
+ limit=warmup_limit,
492
+ start_time=start_time_ms,
493
+ )
494
+
495
+ # Process historical data for warmup (oldest first)
496
+ for kline in historical_klines.values:
497
+ if kline.symbol == symbol and (
498
+ kline.confirm or indicator.warmup_include_unclosed
499
+ ):
500
+ indicator._process_warmup_kline(kline)
501
+
502
+ self._sys_log.debug(
503
+ f"Warmed up indicator {indicator.name} for {symbol} with {len(historical_klines)} klines"
504
+ )
505
+
506
+ except Exception as e:
507
+ self._sys_log.error(
508
+ f"Failed to warm up indicator {indicator.name} for {symbol}: {e}"
509
+ )
510
+
511
+ def get_warmup_status(self) -> dict[Any, list[dict[str, Any]]]:
512
+ """Get the warmup status of all indicators by symbol."""
513
+ status: dict[Any, list[dict[str, Any]]] = {}
514
+ requirements = self._indicator_manager.get_warmup_requirements()
515
+
516
+ for symbol, indicator_list in requirements.items():
517
+ status[symbol] = []
518
+ for indicator, period, interval in indicator_list:
519
+ status[symbol].append(
520
+ {
521
+ "name": indicator.name,
522
+ "warmup_period": period,
523
+ "warmup_interval": interval.value,
524
+ "is_warmed_up": indicator.is_warmed_up,
525
+ "data_count": indicator._warmup_data_count,
526
+ }
527
+ )
528
+
529
+ return status
530
+
531
+ def wait_for_warmup(self, timeout_seconds: int = 60) -> bool:
532
+ """Wait for all indicators to complete warmup. Returns True if all warmed up."""
533
+ start_time = self.clock.timestamp()
534
+ while self.clock.timestamp() - start_time < timeout_seconds:
535
+ if not self._indicator_manager.has_warmup_pending():
536
+ return True
537
+
538
+ return False
539
+
540
+ # Interval-unit -> seconds, used by the fast=True fast-path.
541
+ _INTERVAL_UNITS_S = {
542
+ "weeks": 604800,
543
+ "days": 86400,
544
+ "hours": 3600,
545
+ "minutes": 60,
546
+ "seconds": 1,
547
+ }
548
+
549
+ def schedule(
550
+ self,
551
+ func: Callable[..., Any],
552
+ trigger: Literal["interval", "cron", "date"] = "interval",
553
+ fast: bool = False,
554
+ **kwargs: Any,
555
+ ) -> None:
556
+ """
557
+ There are three modes:
558
+
559
+ - **cron**: run at a specific time second, minute, hour, day, month, year
560
+ - **interval**: run at a specific interval seconds, minutes, hours, days, weeks, months, years
561
+ - **date**: run at a specific date and time, `run_date` must be provided
562
+
563
+ kwargs:
564
+ next_run_time: datetime, when to run the first time
565
+ seconds/minutes/hours/days/weeks: int, interval between runs
566
+ year/month/day/hour/minute/second: int, specific time to run
567
+ args: list, arguments to pass to the function
568
+ kwargs: dict, keyword arguments to pass to the function
569
+
570
+ fast:
571
+ Opt into the on-loop, low-overhead fast-path (interval trigger only),
572
+ backed by ``loop.call_at`` instead of APScheduler. Intended for
573
+ high-frequency (sub-second) jobs, e.g. ``seconds=0.1``. The job runs
574
+ directly ON the event loop, so it MUST be non-blocking: blocking
575
+ ``run_sync``-based facades (``request_klines``/``request_ticker``/...)
576
+ will raise instead of deadlocking — read cached data (``self.cache...``)
577
+ or submit orders (``create_order_ws``) instead. Cadence is fixed-rate
578
+ (drift-free); async jobs skip a tick if the previous run is still in
579
+ flight. See ``_schedule_fast``.
580
+ """
581
+ if not self._initialized:
582
+ raise RuntimeError(
583
+ "Strategy not initialized, please use `schedule` in `on_start` method"
584
+ )
585
+ if fast:
586
+ self._schedule_fast(func, trigger, kwargs)
587
+ return
588
+ if not inspect.iscoroutinefunction(func):
589
+ _args = kwargs.pop("args", ())
590
+ _kwargs = kwargs.pop("kwargs", {})
591
+
592
+ # Run the sync job in a thread pool (Path B). This keeps blocking
593
+ # facades like request_klines (built on run_sync ->
594
+ # run_coroutine_threadsafe().result()) working: they block the worker
595
+ # thread while the loop runs the coroutine. Running the job directly on
596
+ # the loop would deadlock such calls. Order/subscription submits from
597
+ # this thread are safe because EMS/SMS._safe_put marshals the queue put
598
+ # back onto the loop via call_soon_threadsafe.
599
+ async def _wrapper():
600
+ loop = asyncio.get_event_loop()
601
+ try:
602
+ await loop.run_in_executor(None, lambda: func(*_args, **_kwargs))
603
+ except asyncio.CancelledError:
604
+ pass
605
+
606
+ _wrapper.__name__ = getattr(func, "__name__", type(func).__name__)
607
+ self._scheduler.add_job(_wrapper, trigger=trigger, **kwargs)
608
+ else:
609
+ self._scheduler.add_job(func, trigger=trigger, **kwargs)
610
+
611
+ def _schedule_fast(
612
+ self, func: Callable[..., Any], trigger: str, kwargs: dict[str, Any]
613
+ ) -> None:
614
+ """On-loop fast-path for schedule(..., fast=True). See schedule() docstring."""
615
+ if trigger != "interval":
616
+ raise ValueError("fast=True only supports trigger='interval'")
617
+ args = kwargs.pop("args", ())
618
+ fkwargs = kwargs.pop("kwargs", {})
619
+ interval_s = 0.0
620
+ for unit, mult in self._INTERVAL_UNITS_S.items():
621
+ if unit in kwargs:
622
+ interval_s += float(kwargs.pop(unit)) * mult
623
+ if kwargs:
624
+ raise ValueError(f"fast=True does not support kwargs: {list(kwargs)}")
625
+ if interval_s <= 0:
626
+ raise ValueError("fast=True requires a positive interval, e.g. seconds=0.1")
627
+
628
+ is_coro = inspect.iscoroutinefunction(func)
629
+ name = getattr(func, "__name__", type(func).__name__)
630
+ loop = self._task_manager.loop
631
+ state = {"handle": None, "running": False, "cancelled": False, "next": 0.0}
632
+
633
+ async def _run_coro():
634
+ try:
635
+ await func(*args, **fkwargs)
636
+ except Exception as e:
637
+ self.log.error(f"[fast timer:{name}] error: {e!r}")
638
+ finally:
639
+ state["running"] = False
640
+
641
+ def _tick():
642
+ if state["cancelled"]:
643
+ return
644
+ # Fixed-rate: aim for absolute target times so cadence doesn't drift.
645
+ # If we've fallen behind, realign to now+interval (skip missed ticks)
646
+ # rather than firing a burst of catch-up ticks.
647
+ state["next"] += interval_s
648
+ now = loop.time()
649
+ if state["next"] <= now:
650
+ state["next"] = now + interval_s
651
+ state["handle"] = loop.call_at(state["next"], _tick)
652
+
653
+ if is_coro:
654
+ if state["running"]:
655
+ self.log.warning(
656
+ f"[fast timer:{name}] previous run still in flight, skipping tick"
657
+ )
658
+ return
659
+ state["running"] = True
660
+ self._task_manager.create_task(_run_coro())
661
+ else:
662
+ try:
663
+ func(*args, **fkwargs) # ON the loop; must be non-blocking
664
+ except Exception as e:
665
+ self.log.error(f"[fast timer:{name}] error: {e!r}")
666
+
667
+ def _arm():
668
+ state["next"] = loop.time() + interval_s
669
+ state["handle"] = loop.call_at(state["next"], _tick)
670
+ self._fast_timers.append(state)
671
+
672
+ # schedule() runs in the on_start executor thread; loop.call_at is not
673
+ # thread-safe, so marshal the initial arm onto the loop. call_soon_threadsafe
674
+ # is also safe when already on the loop, so no thread check is needed.
675
+ loop.call_soon_threadsafe(_arm)
676
+
677
+ def _cancel_fast_timers(self) -> None:
678
+ """Stop and clear all fast timers. Runs on the loop thread (engine dispose)."""
679
+ for state in self._fast_timers:
680
+ state["cancelled"] = True
681
+ handle = state.get("handle")
682
+ if handle is not None:
683
+ handle.cancel()
684
+ self._fast_timers.clear()
685
+
686
+ def market(self, symbol: str) -> BaseMarket:
687
+ instrument_id = InstrumentId.from_str(symbol)
688
+ exchange = self._exchanges[instrument_id.exchange]
689
+ return exchange.market[instrument_id.symbol]
690
+
691
+ def min_order_amount(self, symbol: str, px: float | None = None) -> Decimal:
692
+ instrument_id = InstrumentId.from_str(symbol)
693
+ ems = self._ems[instrument_id.exchange]
694
+ book = self.cache.bookl1(symbol)
695
+ px = px or (book.mid if book is not None else None)
696
+ if px is None:
697
+ raise ValueError(
698
+ "px must be provided for if you call `min_order_amount` or just set `px`"
699
+ )
700
+ return ems._get_min_order_amount(instrument_id.symbol, self.market(symbol), px)
701
+
702
+ def max_order_amount(
703
+ self, symbol: str, is_market: bool = False, px: float | None = None
704
+ ) -> Decimal:
705
+ instrument_id = InstrumentId.from_str(symbol)
706
+ ems = self._ems[instrument_id.exchange]
707
+ book = self.cache.bookl1(symbol)
708
+ px = px or (book.mid if book is not None else None)
709
+ if px is None:
710
+ raise ValueError(
711
+ "px must be provided for if you call `max_order_amount` or just set `px`"
712
+ )
713
+ return ems._get_max_order_amount(
714
+ instrument_id.symbol, self.market(symbol), is_market, px
715
+ )
716
+
717
+ def amount_to_precision(
718
+ self,
719
+ symbol: str,
720
+ amount: float,
721
+ mode: Literal["round", "ceil", "floor"] = "round",
722
+ ) -> Decimal:
723
+ instrument_id = InstrumentId.from_str(symbol)
724
+ ems = self._ems[instrument_id.exchange]
725
+ return ems._amount_to_precision(instrument_id.symbol, amount, mode)
726
+
727
+ def price_to_precision(
728
+ self,
729
+ symbol: str,
730
+ price: float,
731
+ mode: Literal["round", "ceil", "floor"] = "round",
732
+ ) -> Decimal:
733
+ instrument_id = InstrumentId.from_str(symbol)
734
+ ems = self._ems[instrument_id.exchange]
735
+ return ems._price_to_precision(instrument_id.symbol, price, mode)
736
+
737
+ def create_batch_orders(
738
+ self,
739
+ orders: List[BatchOrder],
740
+ account_type: AccountType | None = None,
741
+ ):
742
+ """
743
+ Create a batch of orders.
744
+
745
+ Args:
746
+ orders (List[BatchOrder]): A list of BatchOrder objects to be submitted.
747
+ account_type (AccountType | None): The account type for the orders. If None, it will auto selected the account_type, but for performance issue, recommend to set.
748
+ """
749
+ batch_orders: list[BatchOrderSubmit] = []
750
+ for order in orders:
751
+ batch_order = BatchOrderSubmit(
752
+ symbol=order.symbol,
753
+ instrument_id=InstrumentId.from_str(order.symbol),
754
+ side=order.side,
755
+ type=order.type,
756
+ oid=self._oidgen.oid,
757
+ amount=order.amount,
758
+ price=order.price,
759
+ time_in_force=order.time_in_force or TimeInForce.GTC,
760
+ reduce_only=order.reduce_only,
761
+ kwargs=order.kwargs,
762
+ )
763
+ batch_orders.append(batch_order)
764
+ self._sys_log.info(
765
+ f"[new batch order] symbol={order.symbol}, oid={batch_order.oid}, side={order.side}, type={order.type}, amount={order.amount}, price={order.price}, time_in_force={order.time_in_force}, reduce_only={order.reduce_only}"
766
+ )
767
+ self._ems[batch_orders[0].instrument_id.exchange]._submit_order(
768
+ batch_orders, SubmitType.BATCH, account_type
769
+ )
770
+ return [order.oid for order in batch_orders]
771
+
772
+ def create_tp_sl_order(
773
+ self,
774
+ symbol: str,
775
+ side: OrderSide,
776
+ type: OrderType,
777
+ amount: Decimal,
778
+ price: Decimal | None = None,
779
+ time_in_force: TimeInForce | None = TimeInForce.GTC,
780
+ tp_order_type: OrderType | None = None,
781
+ tp_trigger_price: Decimal | None = None,
782
+ tp_price: Decimal | None = None,
783
+ tp_trigger_type: TriggerType = TriggerType.LAST_PRICE,
784
+ sl_order_type: OrderType | None = None,
785
+ sl_trigger_price: Decimal | None = None,
786
+ sl_price: Decimal | None = None,
787
+ sl_trigger_type: TriggerType = TriggerType.LAST_PRICE,
788
+ account_type: AccountType | None = None,
789
+ **kwargs,
790
+ ) -> str:
791
+ if tp_order_type is None and tp_trigger_price is not None:
792
+ raise ValueError("tp_order_type is required when tp_trigger_price is set")
793
+ if sl_order_type is None and sl_trigger_price is not None:
794
+ raise ValueError("sl_order_type is required when sl_trigger_price is set")
795
+ order = TakeProfitAndStopLossOrderSubmit(
796
+ symbol=symbol,
797
+ instrument_id=InstrumentId.from_str(symbol),
798
+ side=side,
799
+ type=type,
800
+ oid=self._oidgen.oid,
801
+ amount=amount,
802
+ price=price,
803
+ time_in_force=time_in_force,
804
+ tp_order_type=tp_order_type or OrderType.TAKE_PROFIT_MARKET,
805
+ tp_trigger_price=tp_trigger_price,
806
+ tp_price=tp_price,
807
+ tp_trigger_type=tp_trigger_type,
808
+ sl_order_type=sl_order_type or OrderType.STOP_LOSS_MARKET,
809
+ sl_trigger_price=sl_trigger_price,
810
+ sl_price=sl_price,
811
+ sl_trigger_type=sl_trigger_type,
812
+ kwargs=kwargs,
813
+ )
814
+ self._ems[order.instrument_id.exchange]._submit_order(
815
+ order, SubmitType.TAKE_PROFIT_AND_STOP_LOSS, account_type
816
+ )
817
+ return order.oid
818
+
819
+ def create_order(
820
+ self,
821
+ symbol: str,
822
+ side: OrderSide,
823
+ type: OrderType,
824
+ amount: Decimal,
825
+ price: Decimal | None = None,
826
+ time_in_force: TimeInForce | None = TimeInForce.GTC,
827
+ reduce_only: bool = False,
828
+ account_type: AccountType | None = None,
829
+ **kwargs,
830
+ ) -> str:
831
+ order = CreateOrderSubmit(
832
+ symbol=symbol,
833
+ oid=self._oidgen.oid,
834
+ instrument_id=InstrumentId.from_str(symbol),
835
+ side=side,
836
+ type=type,
837
+ amount=amount,
838
+ price=price,
839
+ time_in_force=time_in_force,
840
+ reduce_only=reduce_only,
841
+ # position_side=position_side,
842
+ kwargs=kwargs,
843
+ )
844
+ self._ems[order.instrument_id.exchange]._submit_order(
845
+ order, SubmitType.CREATE, account_type
846
+ )
847
+ self._sys_log.info(
848
+ f"[new order] symbol={symbol}, oid={order.oid}, side={side}, type={type}, amount={amount}, price={price}, time_in_force={time_in_force}, reduce_only={reduce_only}"
849
+ )
850
+ return order.oid
851
+
852
+ def create_order_ws(
853
+ self,
854
+ symbol: str,
855
+ side: OrderSide,
856
+ type: OrderType,
857
+ amount: Decimal,
858
+ price: Decimal | None = None,
859
+ time_in_force: TimeInForce | None = TimeInForce.GTC,
860
+ reduce_only: bool = False,
861
+ account_type: AccountType | None = None,
862
+ **kwargs,
863
+ ) -> str:
864
+ order = CreateOrderSubmit(
865
+ symbol=symbol,
866
+ oid=self._oidgen.oid,
867
+ instrument_id=InstrumentId.from_str(symbol),
868
+ side=side,
869
+ type=type,
870
+ amount=amount,
871
+ price=price,
872
+ time_in_force=time_in_force,
873
+ reduce_only=reduce_only,
874
+ # position_side=position_side,
875
+ kwargs=kwargs,
876
+ )
877
+ self._ems[order.instrument_id.exchange]._submit_order(
878
+ order, SubmitType.CREATE_WS, account_type
879
+ )
880
+ self._sys_log.info(
881
+ f"[new order ws] symbol={symbol}, oid={order.oid}, side={side}, type={type}, amount={amount}, price={price}, time_in_force={time_in_force}, reduce_only={reduce_only}"
882
+ )
883
+ return order.oid
884
+
885
+ def cancel_order(
886
+ self, symbol: str, oid: str, account_type: AccountType | None = None, **kwargs
887
+ ) -> str:
888
+ self.cache.mark_cancel_intent(oid)
889
+ order = CancelOrderSubmit(
890
+ symbol=symbol,
891
+ instrument_id=InstrumentId.from_str(symbol),
892
+ oid=oid,
893
+ kwargs=kwargs,
894
+ )
895
+ self._ems[order.instrument_id.exchange]._submit_order(
896
+ order, SubmitType.CANCEL, account_type
897
+ )
898
+ self._sys_log.info(f"[cancel order] symbol={symbol}, oid={oid}")
899
+ return order.oid
900
+
901
+ def cancel_batch_orders(
902
+ self,
903
+ orders: List[CancelBatchOrder],
904
+ account_type: AccountType | None = None,
905
+ ) -> List[str]:
906
+ cancel_submits = [
907
+ CancelOrderSubmit(
908
+ symbol=order.symbol,
909
+ instrument_id=InstrumentId.from_str(order.symbol),
910
+ oid=order.oid,
911
+ kwargs=order.kwargs,
912
+ )
913
+ for order in orders
914
+ ]
915
+ if not cancel_submits:
916
+ return []
917
+ for submit in cancel_submits:
918
+ self.cache.mark_cancel_intent(submit.oid)
919
+ self._sys_log.info(
920
+ f"[cancel batch order] symbol={submit.symbol}, oid={submit.oid}"
921
+ )
922
+ self._ems[cancel_submits[0].instrument_id.exchange]._submit_order(
923
+ cancel_submits, SubmitType.CANCEL_BATCH, account_type
924
+ )
925
+ return [s.oid for s in cancel_submits]
926
+
927
+ def cancel_order_ws(
928
+ self, symbol: str, oid: str, account_type: AccountType | None = None, **kwargs
929
+ ) -> str:
930
+ self.cache.mark_cancel_intent(oid)
931
+ order = CancelOrderSubmit(
932
+ symbol=symbol,
933
+ instrument_id=InstrumentId.from_str(symbol),
934
+ oid=oid,
935
+ kwargs=kwargs,
936
+ )
937
+ self._ems[order.instrument_id.exchange]._submit_order(
938
+ order, SubmitType.CANCEL_WS, account_type
939
+ )
940
+ self._sys_log.info(f"[cancel order ws] symbol={symbol}, oid={oid}")
941
+ return order.oid
942
+
943
+ def cancel_all_orders(
944
+ self, symbol: str, account_type: AccountType | None = None
945
+ ) -> str:
946
+ self.cache.mark_all_cancel_intent(symbol)
947
+ order = CancelAllOrderSubmit(
948
+ symbol=symbol,
949
+ instrument_id=InstrumentId.from_str(symbol),
950
+ )
951
+ self._ems[order.instrument_id.exchange]._submit_order(
952
+ order, SubmitType.CANCEL_ALL, account_type
953
+ )
954
+ self._sys_log.info(f"[cancel all orders] symbol={symbol}")
955
+ return symbol
956
+
957
+ def modify_order(
958
+ self,
959
+ symbol: str,
960
+ oid: str,
961
+ side: OrderSide | None = None,
962
+ price: Decimal | None = None,
963
+ amount: Decimal | None = None,
964
+ account_type: AccountType | None = None,
965
+ **kwargs,
966
+ ) -> str:
967
+ if side is None or price is None or amount is None:
968
+ raise ValueError("side, price, and amount are required to modify an order")
969
+ order = ModifyOrderSubmit(
970
+ symbol=symbol,
971
+ instrument_id=InstrumentId.from_str(symbol),
972
+ oid=oid,
973
+ side=side,
974
+ price=price,
975
+ amount=amount,
976
+ kwargs=kwargs,
977
+ )
978
+ self._ems[order.instrument_id.exchange]._submit_order(
979
+ order, SubmitType.MODIFY, account_type
980
+ )
981
+ self._sys_log.info(
982
+ f"[modify order] symbol={symbol}, oid={oid}, side={side}, price={price}, amount={amount}"
983
+ )
984
+ return order.oid
985
+
986
+ def create_algo_order(
987
+ self,
988
+ symbol: str,
989
+ side: OrderSide,
990
+ amount: Decimal,
991
+ exec_algorithm_id: str,
992
+ exec_params: Dict[str, Any],
993
+ reduce_only: bool = False,
994
+ account_type: AccountType | None = None,
995
+ ) -> str:
996
+ """
997
+ Create an order to be executed by an execution algorithm.
998
+
999
+ Parameters
1000
+ ----------
1001
+ symbol : str
1002
+ The trading symbol.
1003
+ side : OrderSide
1004
+ The order side (BUY/SELL).
1005
+ amount : Decimal
1006
+ The total order amount.
1007
+ exec_algorithm_id : str
1008
+ The execution algorithm ID (e.g., "TWAP").
1009
+ exec_params : Dict[str, Any]
1010
+ Parameters for the execution algorithm.
1011
+ reduce_only : bool
1012
+ If True, only reduce position.
1013
+ account_type : AccountType | None
1014
+ The account type.
1015
+
1016
+ Returns
1017
+ -------
1018
+ str
1019
+ The primary order ID.
1020
+
1021
+ Example
1022
+ -------
1023
+ >>> oid = self.create_algo_order(
1024
+ ... symbol="BTCUSDT-PERP.BINANCE",
1025
+ ... side=OrderSide.BUY,
1026
+ ... amount=Decimal("1.0"),
1027
+ ... exec_algorithm_id="TWAP",
1028
+ ... exec_params={
1029
+ ... "horizon_secs": 300, # 5 minutes
1030
+ ... "interval_secs": 30, # every 30 seconds
1031
+ ... },
1032
+ ... )
1033
+ """
1034
+ if exec_algorithm_id not in self._exec_algorithms:
1035
+ raise ValueError(
1036
+ f"Execution algorithm '{exec_algorithm_id}' not registered"
1037
+ )
1038
+
1039
+ oid = self._oidgen.oid
1040
+ instrument_id = InstrumentId.from_str(symbol)
1041
+
1042
+ if not account_type:
1043
+ account_type = self._ems[
1044
+ instrument_id.exchange
1045
+ ]._instrument_id_to_account_type(instrument_id)
1046
+
1047
+ # Create command to send to ExecAlgorithm
1048
+ command = ExecAlgorithmCommand(
1049
+ command_type=ExecAlgorithmCommandType.EXECUTE,
1050
+ exec_algorithm_id=exec_algorithm_id,
1051
+ order_params=ExecAlgorithmOrderParams(
1052
+ oid=oid,
1053
+ symbol=symbol,
1054
+ side=side,
1055
+ amount=amount,
1056
+ account_type=account_type,
1057
+ reduce_only=reduce_only,
1058
+ ),
1059
+ exec_params=exec_params,
1060
+ )
1061
+
1062
+ # Send to execution algorithm via MessageBus
1063
+ self._msgbus.send(
1064
+ endpoint=f"{exec_algorithm_id}.execute",
1065
+ msg=command,
1066
+ )
1067
+
1068
+ self._sys_log.info(
1069
+ f"[algo order] symbol={symbol}, oid={oid}, side={side}, "
1070
+ f"amount={amount}, algorithm={exec_algorithm_id}"
1071
+ )
1072
+
1073
+ return oid
1074
+
1075
+ def cancel_algo_order(self, oid: str, exec_algorithm_id: str):
1076
+ """
1077
+ Cancel an execution algorithm order.
1078
+
1079
+ Parameters
1080
+ ----------
1081
+ oid : str
1082
+ The primary order ID.
1083
+ exec_algorithm_id : str
1084
+ The execution algorithm ID.
1085
+ """
1086
+ if exec_algorithm_id not in self._exec_algorithms:
1087
+ raise ValueError(
1088
+ f"Execution algorithm '{exec_algorithm_id}' not registered"
1089
+ )
1090
+
1091
+ command = ExecAlgorithmCommand(
1092
+ command_type=ExecAlgorithmCommandType.CANCEL,
1093
+ exec_algorithm_id=exec_algorithm_id,
1094
+ primary_oid=oid,
1095
+ )
1096
+
1097
+ self._msgbus.send(
1098
+ endpoint=f"{exec_algorithm_id}.execute",
1099
+ msg=command,
1100
+ )
1101
+
1102
+ self._sys_log.info(
1103
+ f"[cancel algo order] oid={oid}, algorithm={exec_algorithm_id}"
1104
+ )
1105
+
1106
+ def get_algo_order(
1107
+ self, oid: str, exec_algorithm_id: str
1108
+ ) -> ExecAlgorithmOrder | None:
1109
+ """
1110
+ Get an execution algorithm order by its primary order ID.
1111
+
1112
+ Parameters
1113
+ ----------
1114
+ oid : str
1115
+ The primary order ID.
1116
+ exec_algorithm_id : str
1117
+ The execution algorithm ID.
1118
+
1119
+ Returns
1120
+ -------
1121
+ ExecAlgorithmOrder | None
1122
+ The execution algorithm order, or None if not found.
1123
+ """
1124
+ if exec_algorithm_id not in self._exec_algorithms:
1125
+ raise ValueError(
1126
+ f"Execution algorithm '{exec_algorithm_id}' not registered"
1127
+ )
1128
+
1129
+ return self._exec_algorithms[exec_algorithm_id].get_algo_order(oid)
1130
+
1131
+ # def create_twap(
1132
+ # self,
1133
+ # symbol: str,
1134
+ # side: OrderSide,
1135
+ # amount: Decimal,
1136
+ # duration: int,
1137
+ # wait: int,
1138
+ # check_interval: float = 0.1,
1139
+ # position_side: PositionSide | None = None,
1140
+ # account_type: AccountType | None = None,
1141
+ # **kwargs,
1142
+ # ) -> str:
1143
+ # order = TWAPOrderSubmit(
1144
+ # symbol=symbol,
1145
+ # instrument_id=InstrumentId.from_str(symbol),
1146
+ # side=side,
1147
+ # amount=amount,
1148
+ # duration=duration,
1149
+ # wait=wait,
1150
+ # check_interval=check_interval,
1151
+ # position_side=position_side,
1152
+ # kwargs=kwargs,
1153
+ # )
1154
+ # self._ems[order.instrument_id.exchange]._submit_order(
1155
+ # order, SubmitType.TWAP, account_type
1156
+ # )
1157
+ # return order.uuid
1158
+
1159
+ # def cancel_twap(
1160
+ # self, symbol: str, uuid: str, account_type: AccountType | None = None
1161
+ # ) -> str:
1162
+ # order = CancelTWAPOrderSubmit(
1163
+ # symbol=symbol,
1164
+ # instrument_id=InstrumentId.from_str(symbol),
1165
+ # uuid=uuid,
1166
+ # )
1167
+ # self._ems[order.instrument_id.exchange]._submit_order(
1168
+ # order, SubmitType.CANCEL_TWAP, account_type
1169
+ # )
1170
+ # return order.uuid
1171
+
1172
+ def subscribe_bookl1(
1173
+ self, symbols: str | List[str], ready_timeout: int = 60, ready: bool = True
1174
+ ):
1175
+ """
1176
+ Subscribe to level 1 book data for the given symbols.
1177
+
1178
+ Args:
1179
+ symbols (List[str]): The symbols to subscribe to.
1180
+ ready_timeout (int): The timeout for the data to be ready.
1181
+ ready (bool): default is True. Whether the data is ready. If True, the data will be ready immediately. When you use event driven strategy, you can set it to True. Otherwise, set it to False.
1182
+ """
1183
+ if not self._initialized:
1184
+ raise StrategyBuildError(
1185
+ "Strategy not initialized, please use `subscribe_bookl1` in `on_start` method"
1186
+ )
1187
+
1188
+ self._msgbus.subscribe(topic="bookl1", handler=self._on_bookl1)
1189
+
1190
+ self._sms.subscribe(
1191
+ symbols=symbols,
1192
+ data_type=DataType.BOOKL1,
1193
+ ready_timeout=ready_timeout,
1194
+ ready=ready,
1195
+ )
1196
+
1197
+ def subscribe_trade(
1198
+ self, symbols: str | List[str], ready_timeout: int = 60, ready: bool = True
1199
+ ):
1200
+ """
1201
+ Subscribe to trade data for the given symbols.
1202
+
1203
+ Args:
1204
+ symbols (List[str]): The symbols to subscribe to.
1205
+ ready_timeout (int): The timeout for the data to be ready.
1206
+ ready (bool): default is True. Whether the data is ready. If True, the data will be ready immediately. When you use event driven strategy, you can set it to True. Otherwise, set it to False.
1207
+ """
1208
+ if not self._initialized:
1209
+ raise StrategyBuildError(
1210
+ "Strategy not initialized, please use `subscribe_trade` in `on_start` method"
1211
+ )
1212
+
1213
+ self._msgbus.subscribe(topic="trade", handler=self._on_trade)
1214
+
1215
+ self._sms.subscribe(
1216
+ symbols=symbols,
1217
+ data_type=DataType.TRADE,
1218
+ ready_timeout=ready_timeout,
1219
+ ready=ready,
1220
+ )
1221
+
1222
+ def subscribe_kline(
1223
+ self,
1224
+ symbols: str | List[str],
1225
+ interval: KlineInterval,
1226
+ ready_timeout: int = 60,
1227
+ ready: bool = True,
1228
+ use_aggregator: bool = False,
1229
+ build_with_no_updates: bool = True,
1230
+ ):
1231
+ """
1232
+ Subscribe to kline data for the given symbols.
1233
+
1234
+ Args:
1235
+ symbols (List[str]): The symbols to subscribe to.
1236
+ interval (str): The interval of the kline data
1237
+ ready_timeout (int): The timeout for the data to be ready.
1238
+ ready (bool): default is True. Whether the data is ready. If True, the data will be ready immediately. When you use event driven strategy, you can set it to True. Otherwise, set it to False.
1239
+ use_aggregator (bool): If True, use TimeKlineAggregator instead of exchange native klines. Useful when exchange doesn't support certain intervals.
1240
+ """
1241
+ if not self._initialized:
1242
+ raise StrategyBuildError(
1243
+ "Strategy not initialized, please use `subscribe_kline` in `on_start` method"
1244
+ )
1245
+
1246
+ self._msgbus.subscribe(topic="kline", handler=self._on_kline)
1247
+
1248
+ if isinstance(symbols, str):
1249
+ symbols = [symbols]
1250
+
1251
+ self._sms.subscribe(
1252
+ symbols=symbols,
1253
+ data_type=DataType.KLINE,
1254
+ params={
1255
+ "interval": interval,
1256
+ "use_aggregator": use_aggregator,
1257
+ "build_with_no_updates": build_with_no_updates,
1258
+ },
1259
+ ready_timeout=ready_timeout,
1260
+ ready=ready,
1261
+ )
1262
+
1263
+ def subscribe_volume_kline(
1264
+ self,
1265
+ symbols: str | List[str],
1266
+ volume_threshold: float,
1267
+ volume_type: Literal["DEFAULT", "BUY", "SELL"] = "DEFAULT",
1268
+ ready_timeout: int = 60,
1269
+ ready: bool = True,
1270
+ ):
1271
+ """
1272
+ Subscribe to volume-based kline data for the given symbols.
1273
+
1274
+ Args:
1275
+ symbols (List[str]): The symbols to subscribe to.
1276
+ volume_threshold (float): The volume threshold for creating new klines
1277
+ ready_timeout (int): The timeout for the data to be ready.
1278
+ ready (bool): default is True. Whether the data is ready. If True, the data will be ready immediately. When you use event driven strategy, you can set it to True. Otherwise, set it to False.
1279
+ """
1280
+ if not self._initialized:
1281
+ raise StrategyBuildError(
1282
+ "Strategy not initialized, please use `subscribe_volume_kline` in `on_start` method"
1283
+ )
1284
+
1285
+ self._msgbus.subscribe(topic="kline", handler=self._on_kline)
1286
+
1287
+ self._sms.subscribe(
1288
+ symbols=symbols,
1289
+ data_type=DataType.VOLUME_KLINE,
1290
+ params={
1291
+ "volume_threshold": volume_threshold,
1292
+ "volume_type": volume_type,
1293
+ },
1294
+ ready_timeout=ready_timeout,
1295
+ ready=ready,
1296
+ )
1297
+
1298
+ def subscribe_bookl2(
1299
+ self,
1300
+ symbols: str | List[str],
1301
+ level: BookLevel,
1302
+ ready_timeout: int = 60,
1303
+ ready: bool = True,
1304
+ ):
1305
+ """
1306
+ Subscribe to level 2 book data for the given symbols.
1307
+
1308
+ Args:
1309
+ symbols (List[str]): The symbols to subscribe to.
1310
+ level (BookLevel): The level of the book data
1311
+ ready_timeout (int): The timeout for the data to be ready.
1312
+ ready (bool): default is True. Whether the data is ready. If True, the data will be ready immediately. When you use event driven strategy, you can set it to True. Otherwise, set it to False.
1313
+ """
1314
+ if not self._initialized:
1315
+ raise StrategyBuildError(
1316
+ "Strategy not initialized, please use `subscribe_bookl2` in `on_start` method"
1317
+ )
1318
+
1319
+ self._msgbus.subscribe(topic="bookl2", handler=self._on_bookl2)
1320
+
1321
+ self._sms.subscribe(
1322
+ symbols=symbols,
1323
+ data_type=DataType.BOOKL2,
1324
+ params={"level": level},
1325
+ ready_timeout=ready_timeout,
1326
+ ready=ready,
1327
+ )
1328
+
1329
+ def subscribe_funding_rate(
1330
+ self, symbols: str | List[str], ready_timeout: int = 60, ready: bool = True
1331
+ ):
1332
+ """
1333
+ Subscribe to funding rate data for the given symbols.
1334
+
1335
+ Args:
1336
+ symbols (List[str]): The symbols to subscribe to.
1337
+ ready_timeout (int): The timeout for the data to be ready.
1338
+ ready (bool): default is True. Whether the data is ready. If True, the data will be ready immediately. When you use event driven strategy, you can set it to True. Otherwise, set it to False.
1339
+ """
1340
+ if not self._initialized:
1341
+ raise StrategyBuildError(
1342
+ "Strategy not initialized, please use `subscribe_funding_rate` in `on_start` method"
1343
+ )
1344
+
1345
+ self._msgbus.subscribe(topic="funding_rate", handler=self._on_funding_rate)
1346
+
1347
+ self._sms.subscribe(
1348
+ symbols=symbols,
1349
+ data_type=DataType.FUNDING_RATE,
1350
+ ready=ready,
1351
+ ready_timeout=ready_timeout,
1352
+ )
1353
+
1354
+ def subscribe_index_price(
1355
+ self, symbols: str | List[str], ready_timeout: int = 60, ready: bool = True
1356
+ ):
1357
+ """
1358
+ Subscribe to index price data for the given symbols.
1359
+
1360
+ Args:
1361
+ symbols (List[str]): The symbols to subscribe to.
1362
+ ready_timeout (int): The timeout for the data to be ready.
1363
+ ready (bool): default is True. Whether the data is ready. If True, the data will be ready immediately. When you use event driven strategy, you can set it to True. Otherwise, set it to False.
1364
+ """
1365
+ if not self._initialized:
1366
+ raise StrategyBuildError(
1367
+ "Strategy not initialized, please use `subscribe_index_price` in `on_start` method"
1368
+ )
1369
+
1370
+ self._msgbus.subscribe(topic="index_price", handler=self._on_index_price)
1371
+
1372
+ self._sms.subscribe(
1373
+ symbols=symbols,
1374
+ data_type=DataType.INDEX_PRICE,
1375
+ ready=ready,
1376
+ ready_timeout=ready_timeout,
1377
+ )
1378
+
1379
+ def subscribe_mark_price(
1380
+ self, symbols: str | List[str], ready_timeout: int = 60, ready: bool = True
1381
+ ):
1382
+ """
1383
+ Subscribe to mark price data for the given symbols.
1384
+
1385
+ Args:
1386
+ symbols (List[str]): The symbols to subscribe to.
1387
+ ready_timeout (int): The timeout for the data to be ready.
1388
+ ready (bool): default is True. Whether the data is ready. If True, the data will be ready immediately. When you use event driven strategy, you can set it to True. Otherwise, set it to False.
1389
+ """
1390
+ if not self._initialized:
1391
+ raise StrategyBuildError(
1392
+ "Strategy not initialized, please use `subscribe_mark_price` in `on_start` method"
1393
+ )
1394
+
1395
+ self._msgbus.subscribe(topic="mark_price", handler=self._on_mark_price)
1396
+
1397
+ self._sms.subscribe(
1398
+ symbols=symbols,
1399
+ data_type=DataType.MARK_PRICE,
1400
+ ready=ready,
1401
+ ready_timeout=ready_timeout,
1402
+ )
1403
+
1404
+ def unsubscribe_bookl1(self, symbols: str | List[str]):
1405
+ """
1406
+ Unsubscribe from level 1 book data for the given symbols.
1407
+
1408
+ Args:
1409
+ symbols (List[str]): The symbols to unsubscribe from.
1410
+ """
1411
+ if not self._initialized:
1412
+ raise StrategyBuildError(
1413
+ "Strategy not initialized, please use `unsubscribe_bookl1` in a valid method"
1414
+ )
1415
+
1416
+ self._sms.unsubscribe(symbols=symbols, data_type=DataType.BOOKL1)
1417
+
1418
+ def unsubscribe_trade(self, symbols: str | List[str]):
1419
+ """
1420
+ Unsubscribe from trade data for the given symbols.
1421
+
1422
+ Args:
1423
+ symbols (List[str]): The symbols to unsubscribe from.
1424
+ """
1425
+ if not self._initialized:
1426
+ raise StrategyBuildError(
1427
+ "Strategy not initialized, please use `unsubscribe_trade` in a valid method"
1428
+ )
1429
+
1430
+ self._sms.unsubscribe(symbols=symbols, data_type=DataType.TRADE)
1431
+
1432
+ def unsubscribe_kline(
1433
+ self,
1434
+ symbols: str | List[str],
1435
+ interval: KlineInterval,
1436
+ use_aggregator: bool = False,
1437
+ ):
1438
+ """
1439
+ Unsubscribe from kline data for the given symbols.
1440
+
1441
+ Args:
1442
+ symbols (List[str]): The symbols to unsubscribe from.
1443
+ interval (KlineInterval): The interval of the kline data
1444
+ use_aggregator (bool): If True, unsubscribe from TimeKlineAggregator instead of exchange native klines.
1445
+ """
1446
+ if not self._initialized:
1447
+ raise StrategyBuildError(
1448
+ "Strategy not initialized, please use `unsubscribe_kline` in a valid method"
1449
+ )
1450
+
1451
+ self._sms.unsubscribe(
1452
+ symbols=symbols,
1453
+ data_type=DataType.KLINE,
1454
+ params={"interval": interval, "use_aggregator": use_aggregator},
1455
+ )
1456
+
1457
+ def unsubscribe_volume_kline(
1458
+ self,
1459
+ symbols: str | List[str],
1460
+ volume_threshold: float,
1461
+ volume_type: Literal["DEFAULT", "BUY", "SELL"] = "DEFAULT",
1462
+ ):
1463
+ """
1464
+ Unsubscribe from volume-based kline data for the given symbols.
1465
+
1466
+ Args:
1467
+ symbols (List[str]): The symbols to unsubscribe from.
1468
+ volume_threshold (float): The volume threshold for the kline aggregator
1469
+ volume_type (str): The type of volume to use
1470
+ """
1471
+ if not self._initialized:
1472
+ raise StrategyBuildError(
1473
+ "Strategy not initialized, please use `unsubscribe_volume_kline` in a valid method"
1474
+ )
1475
+
1476
+ self._sms.unsubscribe(
1477
+ symbols=symbols,
1478
+ data_type=DataType.VOLUME_KLINE,
1479
+ params={
1480
+ "volume_threshold": volume_threshold,
1481
+ "volume_type": volume_type,
1482
+ },
1483
+ )
1484
+
1485
+ def unsubscribe_bookl2(self, symbols: str | List[str], level: BookLevel):
1486
+ """
1487
+ Unsubscribe from level 2 book data for the given symbols.
1488
+
1489
+ Args:
1490
+ symbols (List[str]): The symbols to unsubscribe from.
1491
+ level (BookLevel): The level of the book data
1492
+ """
1493
+ if not self._initialized:
1494
+ raise StrategyBuildError(
1495
+ "Strategy not initialized, please use `unsubscribe_bookl2` in a valid method"
1496
+ )
1497
+
1498
+ self._sms.unsubscribe(
1499
+ symbols=symbols, data_type=DataType.BOOKL2, params={"level": level}
1500
+ )
1501
+
1502
+ def unsubscribe_funding_rate(self, symbols: str | List[str]):
1503
+ """
1504
+ Unsubscribe from funding rate data for the given symbols.
1505
+
1506
+ Args:
1507
+ symbols (List[str]): The symbols to unsubscribe from.
1508
+ """
1509
+ if not self._initialized:
1510
+ raise StrategyBuildError(
1511
+ "Strategy not initialized, please use `unsubscribe_funding_rate` in a valid method"
1512
+ )
1513
+
1514
+ self._sms.unsubscribe(symbols=symbols, data_type=DataType.FUNDING_RATE)
1515
+
1516
+ def unsubscribe_index_price(self, symbols: str | List[str]):
1517
+ """
1518
+ Unsubscribe from index price data for the given symbols.
1519
+
1520
+ Args:
1521
+ symbols (List[str]): The symbols to unsubscribe from.
1522
+ """
1523
+ if not self._initialized:
1524
+ raise StrategyBuildError(
1525
+ "Strategy not initialized, please use `unsubscribe_index_price` in a valid method"
1526
+ )
1527
+
1528
+ self._sms.unsubscribe(symbols=symbols, data_type=DataType.INDEX_PRICE)
1529
+
1530
+ def unsubscribe_mark_price(self, symbols: str | List[str]):
1531
+ """
1532
+ Unsubscribe from mark price data for the given symbols.
1533
+
1534
+ Args:
1535
+ symbols (List[str]): The symbols to unsubscribe from.
1536
+ """
1537
+ if not self._initialized:
1538
+ raise StrategyBuildError(
1539
+ "Strategy not initialized, please use `unsubscribe_mark_price` in a valid method"
1540
+ )
1541
+
1542
+ self._sms.unsubscribe(symbols=symbols, data_type=DataType.MARK_PRICE)
1543
+
1544
+ def linear_info(
1545
+ self,
1546
+ exchange: ExchangeType,
1547
+ base: str | None = None,
1548
+ quote: str | None = None,
1549
+ exclude: List[str] | None = None,
1550
+ ) -> List[str]:
1551
+ _exchange: ExchangeManager = self._exchanges[exchange]
1552
+ return _exchange.linear(base, quote, exclude)
1553
+
1554
+ def spot_info(
1555
+ self,
1556
+ exchange: ExchangeType,
1557
+ base: str | None = None,
1558
+ quote: str | None = None,
1559
+ exclude: List[str] | None = None,
1560
+ ) -> List[str]:
1561
+ _exchange: ExchangeManager = self._exchanges[exchange]
1562
+ return _exchange.spot(base, quote, exclude)
1563
+
1564
+ def future_info(
1565
+ self,
1566
+ exchange: ExchangeType,
1567
+ base: str | None = None,
1568
+ quote: str | None = None,
1569
+ exclude: List[str] | None = None,
1570
+ ) -> List[str]:
1571
+ _exchange: ExchangeManager = self._exchanges[exchange]
1572
+ return _exchange.future(base, quote, exclude)
1573
+
1574
+ def inverse_info(
1575
+ self,
1576
+ exchange: ExchangeType,
1577
+ base: str | None = None,
1578
+ quote: str | None = None,
1579
+ exclude: List[str] | None = None,
1580
+ ) -> List[str]:
1581
+ _exchange: ExchangeManager = self._exchanges[exchange]
1582
+ return _exchange.inverse(base, quote, exclude)
1583
+
1584
+ def on_start(self):
1585
+ pass
1586
+
1587
+ def on_stop(self):
1588
+ pass
1589
+
1590
+ def _on_start(self):
1591
+ self.on_start()
1592
+ self._started = True
1593
+
1594
+ def _on_stop(self):
1595
+ self.on_stop()
1596
+
1597
+ def on_trade(self, trade: Trade):
1598
+ pass
1599
+
1600
+ def on_bookl1(self, bookl1: BookL1):
1601
+ pass
1602
+
1603
+ def on_bookl2(self, bookl2: BookL2):
1604
+ pass
1605
+
1606
+ def on_kline(self, kline: Kline):
1607
+ pass
1608
+
1609
+ def on_funding_rate(self, funding_rate: FundingRate):
1610
+ pass
1611
+
1612
+ def on_index_price(self, index_price: IndexPrice):
1613
+ pass
1614
+
1615
+ def on_mark_price(self, mark_price: MarkPrice):
1616
+ pass
1617
+
1618
+ def on_pending_order(self, order: Order):
1619
+ pass
1620
+
1621
+ def on_accepted_order(self, order: Order):
1622
+ pass
1623
+
1624
+ def on_partially_filled_order(self, order: Order):
1625
+ pass
1626
+
1627
+ def on_filled_order(self, order: Order):
1628
+ pass
1629
+
1630
+ def on_canceling_order(self, order: Order):
1631
+ pass
1632
+
1633
+ def on_expired_order(self, order: Order):
1634
+ pass
1635
+
1636
+ def on_canceled_order(self, order: Order):
1637
+ pass
1638
+
1639
+ def on_failed_order(self, order: Order):
1640
+ pass
1641
+
1642
+ def on_cancel_failed_order(self, order: Order):
1643
+ pass
1644
+
1645
+ def on_balance(self, balance: AccountBalance):
1646
+ pass
1647
+
1648
+ def stop(self):
1649
+ precise_sleep(
1650
+ milliseconds=200
1651
+ ) # wait for 200ms to ensure all messages are processed
1652
+ os.kill(os.getpid(), signal.SIGINT)
1653
+
1654
+ def wait(
1655
+ self,
1656
+ seconds: float = 0,
1657
+ milliseconds: float = 0,
1658
+ microseconds: float = 0,
1659
+ ):
1660
+ """High-precision wait using Linux ``clock_nanosleep`` (~50-100 µs accuracy).
1661
+
1662
+ Parameters are additive. At least one must be positive.
1663
+
1664
+ Parameters
1665
+ ----------
1666
+ seconds : float
1667
+ Duration in seconds.
1668
+ milliseconds : float
1669
+ Duration in milliseconds.
1670
+ microseconds : float
1671
+ Duration in microseconds.
1672
+ """
1673
+ precise_sleep(
1674
+ seconds=seconds,
1675
+ milliseconds=milliseconds,
1676
+ microseconds=microseconds,
1677
+ )
1678
+
1679
+ def _on_trade(self, trade: Trade):
1680
+ if not self._started:
1681
+ return
1682
+ self.on_trade(trade)
1683
+ self._sms.input(DataType.TRADE, trade)
1684
+
1685
+ def _on_bookl2(self, bookl2: BookL2):
1686
+ if not self._started:
1687
+ return
1688
+ self.on_bookl2(bookl2)
1689
+ self._sms.input(DataType.BOOKL2, bookl2)
1690
+
1691
+ def _on_kline(self, kline: Kline):
1692
+ if not self._started:
1693
+ return
1694
+ self.on_kline(kline)
1695
+ if kline.interval == KlineInterval.VOLUME:
1696
+ self._sms.input(kline.symbol, kline)
1697
+ else:
1698
+ self._sms.input(kline.interval.value, kline)
1699
+
1700
+ def _on_funding_rate(self, funding_rate: FundingRate):
1701
+ if not self._started:
1702
+ return
1703
+ self.on_funding_rate(funding_rate)
1704
+ self._sms.input(DataType.FUNDING_RATE, funding_rate)
1705
+
1706
+ def _on_bookl1(self, bookl1: BookL1):
1707
+ if not self._started:
1708
+ return
1709
+ self.on_bookl1(bookl1)
1710
+ self._sms.input(DataType.BOOKL1, bookl1)
1711
+
1712
+ def _on_index_price(self, index_price: IndexPrice):
1713
+ if not self._started:
1714
+ return
1715
+ self.on_index_price(index_price)
1716
+ self._sms.input(DataType.INDEX_PRICE, index_price)
1717
+
1718
+ def _on_mark_price(self, mark_price: MarkPrice):
1719
+ if not self._started:
1720
+ return
1721
+ self.on_mark_price(mark_price)
1722
+ self._sms.input(DataType.MARK_PRICE, mark_price)
1723
+
1724
+ def param(
1725
+ self,
1726
+ name: str,
1727
+ value: Optional[Any] = None,
1728
+ default: Optional[Any] = None,
1729
+ backend: BACKEND_LITERAL = "memory",
1730
+ ) -> Any:
1731
+ """
1732
+ Get or set a parameter in the cache.
1733
+
1734
+ Args:
1735
+ name: The parameter name
1736
+ value: The parameter value to set. If None, will get the parameter.
1737
+
1738
+ Returns:
1739
+ The parameter value if getting, None if setting.
1740
+
1741
+ Examples:
1742
+ # Set a parameter
1743
+ self.param('rolling_n', 10)
1744
+
1745
+ # Get a parameter
1746
+ rolling_n = self.param('rolling_n')
1747
+ """
1748
+ param_backend = ParamBackend(backend)
1749
+ if value is not None:
1750
+ # Set parameter
1751
+ self.cache.set_param(name, value, param_backend)
1752
+ return None
1753
+ else:
1754
+ # Get parameter
1755
+ return self.cache.get_param(name, default, param_backend)
1756
+
1757
+ def clear_param(
1758
+ self, name: Optional[str] = None, backend: BACKEND_LITERAL = "memory"
1759
+ ) -> None:
1760
+ """
1761
+ Clear parameter(s) from the cache.
1762
+
1763
+ Args:
1764
+ name: The parameter name to clear. If None, clears all parameters.
1765
+
1766
+ Examples:
1767
+ # Clear a specific parameter
1768
+ self.clear_param('rolling_n')
1769
+
1770
+ # Clear all parameters
1771
+ self.clear_param()
1772
+ """
1773
+ self.cache.clear_param(name, ParamBackend(backend))
1774
+
1775
+ def set_timer(
1776
+ self,
1777
+ callback: Callable[..., Any],
1778
+ interval: timedelta,
1779
+ name: str | None = None,
1780
+ start_time: datetime | None = None,
1781
+ stop_time: datetime | None = None,
1782
+ ) -> None:
1783
+ """
1784
+ Set a timer that calls a callback function at regular intervals.
1785
+
1786
+ Args:
1787
+ callback: The function to call
1788
+ interval: Time interval between calls
1789
+ name: Optional timer name. If not provided, uses the callback function name.
1790
+ start_time: When to start the timer (defaults to now + interval)
1791
+ stop_time: When to stop the timer (optional)
1792
+ """
1793
+ if name is None:
1794
+ name = getattr(callback, "__name__", type(callback).__name__)
1795
+
1796
+ if start_time is None:
1797
+ start_time = self.clock.utc_now() + interval
1798
+
1799
+ self.clock.set_timer(
1800
+ name=name,
1801
+ interval=interval,
1802
+ start_time=start_time,
1803
+ stop_time=stop_time,
1804
+ callback=callback,
1805
+ )