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/strategy.py
ADDED
|
@@ -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
|
+
)
|