Qubx 0.5.7__cp312-cp312-manylinux_2_39_x86_64.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.
Potentially problematic release.
This version of Qubx might be problematic. Click here for more details.
- qubx/__init__.py +207 -0
- qubx/_nb_magic.py +100 -0
- qubx/backtester/__init__.py +5 -0
- qubx/backtester/account.py +145 -0
- qubx/backtester/broker.py +87 -0
- qubx/backtester/data.py +296 -0
- qubx/backtester/management.py +378 -0
- qubx/backtester/ome.py +296 -0
- qubx/backtester/optimization.py +201 -0
- qubx/backtester/simulated_data.py +558 -0
- qubx/backtester/simulator.py +362 -0
- qubx/backtester/utils.py +780 -0
- qubx/cli/__init__.py +0 -0
- qubx/cli/commands.py +67 -0
- qubx/connectors/ccxt/__init__.py +0 -0
- qubx/connectors/ccxt/account.py +495 -0
- qubx/connectors/ccxt/broker.py +132 -0
- qubx/connectors/ccxt/customizations.py +193 -0
- qubx/connectors/ccxt/data.py +612 -0
- qubx/connectors/ccxt/exceptions.py +17 -0
- qubx/connectors/ccxt/factory.py +93 -0
- qubx/connectors/ccxt/utils.py +307 -0
- qubx/core/__init__.py +0 -0
- qubx/core/account.py +251 -0
- qubx/core/basics.py +850 -0
- qubx/core/context.py +420 -0
- qubx/core/exceptions.py +38 -0
- qubx/core/helpers.py +480 -0
- qubx/core/interfaces.py +1150 -0
- qubx/core/loggers.py +514 -0
- qubx/core/lookups.py +475 -0
- qubx/core/metrics.py +1512 -0
- qubx/core/mixins/__init__.py +13 -0
- qubx/core/mixins/market.py +94 -0
- qubx/core/mixins/processing.py +428 -0
- qubx/core/mixins/subscription.py +203 -0
- qubx/core/mixins/trading.py +88 -0
- qubx/core/mixins/universe.py +270 -0
- qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/core/series.pxd +125 -0
- qubx/core/series.pyi +118 -0
- qubx/core/series.pyx +988 -0
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/core/utils.pyi +6 -0
- qubx/core/utils.pyx +62 -0
- qubx/data/__init__.py +25 -0
- qubx/data/helpers.py +416 -0
- qubx/data/readers.py +1562 -0
- qubx/data/tardis.py +100 -0
- qubx/gathering/simplest.py +88 -0
- qubx/math/__init__.py +3 -0
- qubx/math/stats.py +129 -0
- qubx/pandaz/__init__.py +23 -0
- qubx/pandaz/ta.py +2757 -0
- qubx/pandaz/utils.py +638 -0
- qubx/resources/instruments/symbols-binance.cm.json +1 -0
- qubx/resources/instruments/symbols-binance.json +1 -0
- qubx/resources/instruments/symbols-binance.um.json +1 -0
- qubx/resources/instruments/symbols-bitfinex.f.json +1 -0
- qubx/resources/instruments/symbols-bitfinex.json +1 -0
- qubx/resources/instruments/symbols-kraken.f.json +1 -0
- qubx/resources/instruments/symbols-kraken.json +1 -0
- qubx/ta/__init__.py +0 -0
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/ta/indicators.pxd +149 -0
- qubx/ta/indicators.pyi +41 -0
- qubx/ta/indicators.pyx +787 -0
- qubx/trackers/__init__.py +3 -0
- qubx/trackers/abvanced.py +236 -0
- qubx/trackers/composite.py +146 -0
- qubx/trackers/rebalancers.py +129 -0
- qubx/trackers/riskctrl.py +641 -0
- qubx/trackers/sizers.py +235 -0
- qubx/utils/__init__.py +5 -0
- qubx/utils/_pyxreloader.py +281 -0
- qubx/utils/charting/lookinglass.py +1057 -0
- qubx/utils/charting/mpl_helpers.py +1183 -0
- qubx/utils/marketdata/binance.py +284 -0
- qubx/utils/marketdata/ccxt.py +90 -0
- qubx/utils/marketdata/dukas.py +130 -0
- qubx/utils/misc.py +541 -0
- qubx/utils/ntp.py +63 -0
- qubx/utils/numbers_utils.py +7 -0
- qubx/utils/orderbook.py +491 -0
- qubx/utils/plotting/__init__.py +0 -0
- qubx/utils/plotting/dashboard.py +150 -0
- qubx/utils/plotting/data.py +137 -0
- qubx/utils/plotting/interfaces.py +25 -0
- qubx/utils/plotting/renderers/__init__.py +0 -0
- qubx/utils/plotting/renderers/plotly.py +0 -0
- qubx/utils/runner/__init__.py +1 -0
- qubx/utils/runner/_jupyter_runner.pyt +60 -0
- qubx/utils/runner/accounts.py +88 -0
- qubx/utils/runner/configs.py +65 -0
- qubx/utils/runner/runner.py +470 -0
- qubx/utils/time.py +312 -0
- qubx-0.5.7.dist-info/METADATA +105 -0
- qubx-0.5.7.dist-info/RECORD +100 -0
- qubx-0.5.7.dist-info/WHEEL +4 -0
- qubx-0.5.7.dist-info/entry_points.txt +3 -0
qubx/core/interfaces.py
ADDED
|
@@ -0,0 +1,1150 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module defines interfaces and classes related to trading strategies.
|
|
3
|
+
|
|
4
|
+
This module includes:
|
|
5
|
+
- Trading service providers
|
|
6
|
+
- Broker service providers
|
|
7
|
+
- Market data providers
|
|
8
|
+
- Strategy contexts
|
|
9
|
+
- Position tracking and management
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import traceback
|
|
13
|
+
from typing import Any, Dict, List, Literal, Set, Tuple
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pandas as pd
|
|
17
|
+
|
|
18
|
+
from qubx import logger
|
|
19
|
+
from qubx.core.basics import (
|
|
20
|
+
AssetBalance,
|
|
21
|
+
CtrlChannel,
|
|
22
|
+
Deal,
|
|
23
|
+
Instrument,
|
|
24
|
+
ITimeProvider,
|
|
25
|
+
MarketEvent,
|
|
26
|
+
MarketType,
|
|
27
|
+
Order,
|
|
28
|
+
OrderRequest,
|
|
29
|
+
Position,
|
|
30
|
+
Signal,
|
|
31
|
+
TargetPosition,
|
|
32
|
+
Timestamped,
|
|
33
|
+
TriggerEvent,
|
|
34
|
+
dt_64,
|
|
35
|
+
)
|
|
36
|
+
from qubx.core.helpers import set_parameters_to_object
|
|
37
|
+
from qubx.core.series import OHLCV, Bar, Quote
|
|
38
|
+
|
|
39
|
+
RemovalPolicy = Literal["close", "wait_for_close", "wait_for_change"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class IAccountViewer:
|
|
43
|
+
account_id: str
|
|
44
|
+
|
|
45
|
+
def get_base_currency(self) -> str:
|
|
46
|
+
"""Get the base currency for the account.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
str: The base currency.
|
|
50
|
+
"""
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
########################################################
|
|
54
|
+
# Capital information
|
|
55
|
+
########################################################
|
|
56
|
+
def get_capital(self) -> float:
|
|
57
|
+
"""Get the available free capital in the account.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
float: The amount of free capital available for trading
|
|
61
|
+
"""
|
|
62
|
+
...
|
|
63
|
+
|
|
64
|
+
def get_total_capital(self) -> float:
|
|
65
|
+
"""Get the total capital in the account including positions value.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
float: Total account capital
|
|
69
|
+
"""
|
|
70
|
+
...
|
|
71
|
+
|
|
72
|
+
########################################################
|
|
73
|
+
# Balance and position information
|
|
74
|
+
########################################################
|
|
75
|
+
def get_balances(self) -> dict[str, AssetBalance]:
|
|
76
|
+
"""Get all currency balances.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
dict[str, AssetBalance]: Dictionary mapping currency codes to AssetBalance objects
|
|
80
|
+
"""
|
|
81
|
+
...
|
|
82
|
+
|
|
83
|
+
def get_positions(self) -> dict[Instrument, Position]:
|
|
84
|
+
"""Get all current positions.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
dict[Instrument, Position]: Dictionary mapping instruments to their positions
|
|
88
|
+
"""
|
|
89
|
+
...
|
|
90
|
+
|
|
91
|
+
def get_position(self, instrument: Instrument) -> Position:
|
|
92
|
+
"""Get the current position for a specific instrument.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
instrument: The instrument to get the position for
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Position: The position object
|
|
99
|
+
"""
|
|
100
|
+
...
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def positions(self) -> dict[Instrument, Position]:
|
|
104
|
+
"""[Deprecated: Use get_positions()] Get all current positions.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
dict[Instrument, Position]: Dictionary mapping instruments to their positions
|
|
108
|
+
"""
|
|
109
|
+
return self.get_positions()
|
|
110
|
+
|
|
111
|
+
def get_orders(self, instrument: Instrument | None = None) -> dict[str, Order]:
|
|
112
|
+
"""Get active orders, optionally filtered by instrument.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
instrument: Optional instrument to filter orders by
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
dict[str, Order]: Dictionary mapping order IDs to Order objects
|
|
119
|
+
"""
|
|
120
|
+
...
|
|
121
|
+
|
|
122
|
+
def position_report(self) -> dict:
|
|
123
|
+
"""Get detailed report of all positions.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
dict: Dictionary containing position details including quantities, prices, PnL etc.
|
|
127
|
+
"""
|
|
128
|
+
...
|
|
129
|
+
|
|
130
|
+
########################################################
|
|
131
|
+
# Leverage information
|
|
132
|
+
########################################################
|
|
133
|
+
def get_leverage(self, instrument: Instrument) -> float:
|
|
134
|
+
"""Get the leverage used for a specific instrument.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
instrument: The instrument to check
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
float: Current leverage ratio for the instrument
|
|
141
|
+
"""
|
|
142
|
+
...
|
|
143
|
+
|
|
144
|
+
def get_leverages(self) -> dict[Instrument, float]:
|
|
145
|
+
"""Get leverages for all instruments.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
dict[Instrument, float]: Dictionary mapping instruments to their leverage ratios
|
|
149
|
+
"""
|
|
150
|
+
...
|
|
151
|
+
|
|
152
|
+
def get_net_leverage(self) -> float:
|
|
153
|
+
"""Get the net leverage across all positions.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
float: Net leverage ratio
|
|
157
|
+
"""
|
|
158
|
+
...
|
|
159
|
+
|
|
160
|
+
def get_gross_leverage(self) -> float:
|
|
161
|
+
"""Get the gross leverage across all positions.
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
float: Gross leverage ratio
|
|
165
|
+
"""
|
|
166
|
+
...
|
|
167
|
+
|
|
168
|
+
########################################################
|
|
169
|
+
# Margin information
|
|
170
|
+
# Used for margin, swap, futures, options trading
|
|
171
|
+
########################################################
|
|
172
|
+
def get_total_required_margin(self) -> float:
|
|
173
|
+
"""Get total margin required for all positions.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
float: Total required margin
|
|
177
|
+
"""
|
|
178
|
+
...
|
|
179
|
+
|
|
180
|
+
def get_available_margin(self) -> float:
|
|
181
|
+
"""Get available margin for new positions.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
float: Available margin
|
|
185
|
+
"""
|
|
186
|
+
...
|
|
187
|
+
|
|
188
|
+
def get_margin_ratio(self) -> float:
|
|
189
|
+
"""Get current margin ratio.
|
|
190
|
+
|
|
191
|
+
Formula: (total capital + positions value) / total required margin
|
|
192
|
+
|
|
193
|
+
Example:
|
|
194
|
+
If total capital is 1000, positions value is 2000, and total required margin is 3000,
|
|
195
|
+
the margin ratio would be (1000 + 2000) / 3000 = 1.0
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
float: Current margin ratio
|
|
199
|
+
"""
|
|
200
|
+
...
|
|
201
|
+
|
|
202
|
+
def get_reserved(self, instrument: Instrument) -> float:
|
|
203
|
+
"""[Deprecated] Get reserved margin for a specific instrument.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
instrument: The instrument to check
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
float: Reserved margin for the instrument
|
|
210
|
+
"""
|
|
211
|
+
return 0.0
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class IBroker:
|
|
215
|
+
"""Broker provider interface for managing trading operations.
|
|
216
|
+
|
|
217
|
+
Handles account operations, order placement, and position tracking.
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
channel: CtrlChannel
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def is_simulated_trading(self) -> bool:
|
|
224
|
+
"""
|
|
225
|
+
Check if the broker is in simulation mode.
|
|
226
|
+
"""
|
|
227
|
+
...
|
|
228
|
+
|
|
229
|
+
# TODO: think about replacing with async methods
|
|
230
|
+
def send_order(
|
|
231
|
+
self,
|
|
232
|
+
instrument: Instrument,
|
|
233
|
+
order_side: str,
|
|
234
|
+
order_type: str,
|
|
235
|
+
amount: float,
|
|
236
|
+
price: float | None = None,
|
|
237
|
+
client_id: str | None = None,
|
|
238
|
+
time_in_force: str = "gtc",
|
|
239
|
+
**optional,
|
|
240
|
+
) -> Order:
|
|
241
|
+
"""Sends an order to the trading service.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
instrument: The instrument to trade.
|
|
245
|
+
order_side: Order side ("buy" or "sell").
|
|
246
|
+
order_type: Type of order ("market" or "limit").
|
|
247
|
+
amount: Amount of instrument to trade.
|
|
248
|
+
price: Price for limit orders.
|
|
249
|
+
client_id: Client-specified order ID.
|
|
250
|
+
time_in_force: Time in force for order (default: "gtc").
|
|
251
|
+
**optional: Additional order parameters.
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
Order: The created order object.
|
|
255
|
+
"""
|
|
256
|
+
raise NotImplementedError("send_order is not implemented")
|
|
257
|
+
|
|
258
|
+
def cancel_order(self, order_id: str) -> Order | None:
|
|
259
|
+
"""Cancel an existing order.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
order_id: The ID of the order to cancel.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
Order | None: The cancelled Order object if successful, None otherwise.
|
|
266
|
+
"""
|
|
267
|
+
raise NotImplementedError("cancel_order is not implemented")
|
|
268
|
+
|
|
269
|
+
def cancel_orders(self, instrument: Instrument) -> None:
|
|
270
|
+
"""Cancel all orders for an instrument.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
instrument: The instrument to cancel orders for.
|
|
274
|
+
"""
|
|
275
|
+
raise NotImplementedError("cancel_orders is not implemented")
|
|
276
|
+
|
|
277
|
+
def update_order(self, order_id: str, price: float | None = None, amount: float | None = None) -> Order:
|
|
278
|
+
"""Update an existing order.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
order_id: The ID of the order to update.
|
|
282
|
+
price: New price for the order.
|
|
283
|
+
amount: New amount for the order.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
Order: The updated Order object if successful
|
|
287
|
+
|
|
288
|
+
Raises:
|
|
289
|
+
NotImplementedError: If the method is not implemented
|
|
290
|
+
OrderNotFound: If the order is not found
|
|
291
|
+
BadRequest: If the request is invalid
|
|
292
|
+
"""
|
|
293
|
+
raise NotImplementedError("update_order is not implemented")
|
|
294
|
+
|
|
295
|
+
def exchange(self) -> str:
|
|
296
|
+
"""
|
|
297
|
+
Return the name of the exchange this broker is connected to.
|
|
298
|
+
"""
|
|
299
|
+
raise NotImplementedError("exchange() is not implemented")
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class IDataProvider:
|
|
303
|
+
time_provider: ITimeProvider
|
|
304
|
+
channel: CtrlChannel
|
|
305
|
+
|
|
306
|
+
def subscribe(
|
|
307
|
+
self,
|
|
308
|
+
subscription_type: str,
|
|
309
|
+
instruments: Set[Instrument],
|
|
310
|
+
reset: bool = False,
|
|
311
|
+
) -> None:
|
|
312
|
+
"""
|
|
313
|
+
Subscribe to market data for a list of instruments.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
subscription_type: Type of subscription
|
|
317
|
+
instruments: Set of instruments to subscribe to
|
|
318
|
+
reset: Reset existing instruments for the subscription type. Default is False.
|
|
319
|
+
"""
|
|
320
|
+
...
|
|
321
|
+
|
|
322
|
+
def unsubscribe(self, subscription_type: str | None, instruments: Set[Instrument]) -> None:
|
|
323
|
+
"""
|
|
324
|
+
Unsubscribe from market data for a list of instruments.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
subscription_type: Type of subscription to unsubscribe from (optional)
|
|
328
|
+
instruments: Set of instruments to unsubscribe from
|
|
329
|
+
"""
|
|
330
|
+
...
|
|
331
|
+
|
|
332
|
+
def has_subscription(self, instrument: Instrument, subscription_type: str) -> bool:
|
|
333
|
+
"""
|
|
334
|
+
Check if an instrument has a subscription.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
instrument: Instrument to check
|
|
338
|
+
subscription_type: Type of subscription to check
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
bool: True if instrument has the subscription
|
|
342
|
+
"""
|
|
343
|
+
...
|
|
344
|
+
|
|
345
|
+
def get_subscriptions(self, instrument: Instrument | None = None) -> List[str]:
|
|
346
|
+
"""
|
|
347
|
+
Get all subscriptions for an instrument.
|
|
348
|
+
|
|
349
|
+
Args:
|
|
350
|
+
instrument (optional): Instrument to get subscriptions for. If None, all subscriptions are returned.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
List[str]: List of subscriptions
|
|
354
|
+
"""
|
|
355
|
+
...
|
|
356
|
+
|
|
357
|
+
def get_subscribed_instruments(self, subscription_type: str | None = None) -> List[Instrument]:
|
|
358
|
+
"""
|
|
359
|
+
Get a list of instruments that are subscribed to a specific subscription type.
|
|
360
|
+
|
|
361
|
+
Args:
|
|
362
|
+
subscription_type: Type of subscription to filter by (optional)
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
List[Instrument]: List of subscribed instruments
|
|
366
|
+
"""
|
|
367
|
+
...
|
|
368
|
+
|
|
369
|
+
def warmup(self, configs: Dict[Tuple[str, Instrument], str]) -> None:
|
|
370
|
+
"""
|
|
371
|
+
Run warmup for subscriptions.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
configs: Dictionary of (subscription type, instrument) pairs and warmup periods.
|
|
375
|
+
|
|
376
|
+
Example:
|
|
377
|
+
warmup({
|
|
378
|
+
(DataType.OHLC["1h"], instr1): "30d",
|
|
379
|
+
(DataType.OHLC["1Min"], instr1): "6h",
|
|
380
|
+
(DataType.OHLC["1Sec"], instr2): "5Min",
|
|
381
|
+
(DataType.TRADE, instr2): "1h",
|
|
382
|
+
})
|
|
383
|
+
"""
|
|
384
|
+
...
|
|
385
|
+
|
|
386
|
+
def get_ohlc(self, instrument: Instrument, timeframe: str, nbarsback: int) -> list[Bar]:
|
|
387
|
+
"""
|
|
388
|
+
Get historical OHLC data for an instrument.
|
|
389
|
+
"""
|
|
390
|
+
...
|
|
391
|
+
|
|
392
|
+
def get_quote(self, instrument: Instrument) -> Quote:
|
|
393
|
+
"""
|
|
394
|
+
Get the latest quote for an instrument.
|
|
395
|
+
"""
|
|
396
|
+
...
|
|
397
|
+
|
|
398
|
+
@property
|
|
399
|
+
def is_simulation(self) -> bool:
|
|
400
|
+
"""
|
|
401
|
+
Check if data provider is in simulation mode.
|
|
402
|
+
"""
|
|
403
|
+
...
|
|
404
|
+
|
|
405
|
+
def close(self):
|
|
406
|
+
"""
|
|
407
|
+
Close the data provider.
|
|
408
|
+
"""
|
|
409
|
+
...
|
|
410
|
+
|
|
411
|
+
def exchange(self) -> str:
|
|
412
|
+
"""
|
|
413
|
+
Return the name of the exchange this provider reads data
|
|
414
|
+
"""
|
|
415
|
+
raise NotImplementedError("exchange() is not implemented")
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
class IMarketManager(ITimeProvider):
|
|
419
|
+
"""Interface for market data providing class"""
|
|
420
|
+
|
|
421
|
+
def ohlc(self, instrument: Instrument, timeframe: str | None = None, length: int | None = None) -> OHLCV:
|
|
422
|
+
"""Get OHLCV data for an instrument. If length is larger then available cached data, it will be requested from the broker.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
instrument: The instrument to get data for
|
|
426
|
+
timeframe (optional): The timeframe of the data. If None, the default timeframe is used.
|
|
427
|
+
length (optional): Number of bars to retrieve. If None, full cached data is returned.
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
OHLCV: The OHLCV data series
|
|
431
|
+
"""
|
|
432
|
+
...
|
|
433
|
+
|
|
434
|
+
def quote(self, instrument: Instrument) -> Quote | None:
|
|
435
|
+
"""Get latest quote for an instrument.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
instrument: The instrument to get quote for
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
Quote | None: The latest quote or None if not available
|
|
442
|
+
"""
|
|
443
|
+
...
|
|
444
|
+
|
|
445
|
+
def get_data(self, instrument: Instrument, sub_type: str) -> list[Any]:
|
|
446
|
+
"""Get data for an instrument. This method is used for getting data for custom subscription types.
|
|
447
|
+
Could be used for orderbook, trades, liquidations, funding rates, etc.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
instrument: The instrument to get data for
|
|
451
|
+
sub_type: The subscription type of data to get
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
List[Any]: The data
|
|
455
|
+
"""
|
|
456
|
+
...
|
|
457
|
+
|
|
458
|
+
def get_aux_data(self, data_id: str, **parametes) -> pd.DataFrame | None:
|
|
459
|
+
"""Get auxiliary data by ID.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
data_id: Identifier for the auxiliary data
|
|
463
|
+
**parametes: Additional parameters for the data request
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
pd.DataFrame | None: The auxiliary data or None if not found
|
|
467
|
+
"""
|
|
468
|
+
...
|
|
469
|
+
|
|
470
|
+
def get_instruments(self) -> list[Instrument]:
|
|
471
|
+
"""Get list of subscribed instruments.
|
|
472
|
+
|
|
473
|
+
Returns:
|
|
474
|
+
list[Instrument]: List of subscribed instruments
|
|
475
|
+
"""
|
|
476
|
+
...
|
|
477
|
+
|
|
478
|
+
def query_instrument(self, symbol: str, exchange: str | None = None) -> Instrument | None:
|
|
479
|
+
"""Query instrument in lookup by symbol and exchange.
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
symbol: The symbol to look up
|
|
483
|
+
exchange: The exchange to look up or None (current exchange is used)
|
|
484
|
+
|
|
485
|
+
Returns:
|
|
486
|
+
Instrument | None: The instrument if found, None otherwise
|
|
487
|
+
"""
|
|
488
|
+
...
|
|
489
|
+
|
|
490
|
+
def exchanges(self) -> list[str]: ...
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
class ITradingManager:
|
|
494
|
+
"""Manages order operations."""
|
|
495
|
+
|
|
496
|
+
def trade(
|
|
497
|
+
self,
|
|
498
|
+
instrument: Instrument,
|
|
499
|
+
amount: float,
|
|
500
|
+
price: float | None = None,
|
|
501
|
+
time_in_force="gtc",
|
|
502
|
+
**options,
|
|
503
|
+
) -> Order:
|
|
504
|
+
"""Place a trade order.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
instrument: The instrument to trade
|
|
508
|
+
amount: Amount to trade (positive for buy, negative for sell)
|
|
509
|
+
price: Optional limit price
|
|
510
|
+
time_in_force: Time in force for the order
|
|
511
|
+
**options: Additional order options
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
Order: The created order
|
|
515
|
+
"""
|
|
516
|
+
...
|
|
517
|
+
|
|
518
|
+
def submit_orders(self, order_requests: list[OrderRequest]) -> list[Order]:
|
|
519
|
+
"""Submit multiple orders to the exchange."""
|
|
520
|
+
...
|
|
521
|
+
|
|
522
|
+
def set_target_position(
|
|
523
|
+
self, instrument: Instrument, target: float, price: float | None = None, **options
|
|
524
|
+
) -> Order:
|
|
525
|
+
"""Set target position for an instrument.
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
instrument: The instrument to set target position for
|
|
529
|
+
target: Target position size
|
|
530
|
+
price: Optional limit price
|
|
531
|
+
time_in_force: Time in force for the order
|
|
532
|
+
**options: Additional order options
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
Order: The created order
|
|
536
|
+
"""
|
|
537
|
+
...
|
|
538
|
+
|
|
539
|
+
def close_position(self, instrument: Instrument) -> None:
|
|
540
|
+
"""Close position for an instrument.
|
|
541
|
+
|
|
542
|
+
Args:
|
|
543
|
+
instrument: The instrument to close position for
|
|
544
|
+
"""
|
|
545
|
+
...
|
|
546
|
+
|
|
547
|
+
def close_positions(self, market_type: MarketType | None = None) -> None:
|
|
548
|
+
"""Close all positions."""
|
|
549
|
+
...
|
|
550
|
+
|
|
551
|
+
def cancel_order(self, order_id: str) -> None:
|
|
552
|
+
"""Cancel a specific order.
|
|
553
|
+
|
|
554
|
+
Args:
|
|
555
|
+
order_id: ID of the order to cancel
|
|
556
|
+
"""
|
|
557
|
+
...
|
|
558
|
+
|
|
559
|
+
def cancel_orders(self, instrument: Instrument) -> None:
|
|
560
|
+
"""Cancel all orders for an instrument.
|
|
561
|
+
|
|
562
|
+
Args:
|
|
563
|
+
instrument: The instrument to cancel orders for
|
|
564
|
+
"""
|
|
565
|
+
...
|
|
566
|
+
|
|
567
|
+
def exchanges(self) -> list[str]: ...
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
class IUniverseManager:
|
|
571
|
+
"""Manages universe updates."""
|
|
572
|
+
|
|
573
|
+
def set_universe(
|
|
574
|
+
self, instruments: list[Instrument], skip_callback: bool = False, if_has_position_then: RemovalPolicy = "close"
|
|
575
|
+
):
|
|
576
|
+
"""Set the trading universe.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
instruments: List of instruments in the universe
|
|
580
|
+
skip_callback: Skip callback to the strategy
|
|
581
|
+
if_has_position_then: What to do if the instrument has a position
|
|
582
|
+
- “close” (default) - close position immediatelly and remove (unsubscribe) instrument from strategy
|
|
583
|
+
- “wait_for_close” - keep instrument and it’s position until it’s closed from strategy (or risk management), then remove instrument from strategy
|
|
584
|
+
- “wait_for_change” - keep instrument and position until strategy would try to change it - then close position and remove instrument
|
|
585
|
+
"""
|
|
586
|
+
...
|
|
587
|
+
|
|
588
|
+
def add_instruments(self, instruments: list[Instrument]):
|
|
589
|
+
"""Add instruments to the trading universe.
|
|
590
|
+
|
|
591
|
+
Args:
|
|
592
|
+
instruments: List of instruments to add
|
|
593
|
+
"""
|
|
594
|
+
...
|
|
595
|
+
|
|
596
|
+
def remove_instruments(self, instruments: list[Instrument], if_has_position_then: RemovalPolicy = "close"):
|
|
597
|
+
"""Remove instruments from the trading universe.
|
|
598
|
+
|
|
599
|
+
Args:
|
|
600
|
+
instruments: List of instruments to remove
|
|
601
|
+
if_has_position_then: What to do if the instrument has a position
|
|
602
|
+
- “close” (default) - close position immediatelly and remove (unsubscribe) instrument from strategy
|
|
603
|
+
- “wait_for_close” - keep instrument and it’s position until it’s closed from strategy (or risk management), then remove instrument from strategy
|
|
604
|
+
- “wait_for_change” - keep instrument and position until strategy would try to change it - then close position and remove instrument
|
|
605
|
+
"""
|
|
606
|
+
...
|
|
607
|
+
|
|
608
|
+
@property
|
|
609
|
+
def instruments(self) -> list[Instrument]:
|
|
610
|
+
"""
|
|
611
|
+
Get the list of instruments in the universe.
|
|
612
|
+
"""
|
|
613
|
+
...
|
|
614
|
+
|
|
615
|
+
def on_alter_position(self, instrument: Instrument) -> None:
|
|
616
|
+
"""
|
|
617
|
+
Called when the position of an instrument changes.
|
|
618
|
+
It can be used for postponed unsubscribed events
|
|
619
|
+
"""
|
|
620
|
+
...
|
|
621
|
+
|
|
622
|
+
def is_trading_allowed(self, instrument: Instrument) -> bool:
|
|
623
|
+
"""
|
|
624
|
+
Check if trading is allowed for an instrument because of the instrument's trading policy.
|
|
625
|
+
"""
|
|
626
|
+
...
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
class ISubscriptionManager:
|
|
630
|
+
"""Manages subscriptions."""
|
|
631
|
+
|
|
632
|
+
def subscribe(self, subscription_type: str, instruments: List[Instrument] | Instrument | None = None) -> None:
|
|
633
|
+
"""Subscribe to market data for an instrument.
|
|
634
|
+
|
|
635
|
+
Args:
|
|
636
|
+
subscription_type: Type of subscription. If None, the base subscription type is used.
|
|
637
|
+
instruments: A list of instrument of instrument to subscribe to
|
|
638
|
+
"""
|
|
639
|
+
...
|
|
640
|
+
|
|
641
|
+
def unsubscribe(self, subscription_type: str, instruments: List[Instrument] | Instrument | None = None) -> None:
|
|
642
|
+
"""Unsubscribe from market data for an instrument.
|
|
643
|
+
|
|
644
|
+
Args:
|
|
645
|
+
subscription_type: Type of subscription to unsubscribe from (e.g. DataType.OHLC)
|
|
646
|
+
instruments (optional): A list of instruments or instrument to unsubscribe from.
|
|
647
|
+
"""
|
|
648
|
+
...
|
|
649
|
+
|
|
650
|
+
def has_subscription(self, instrument: Instrument, subscription_type: str) -> bool:
|
|
651
|
+
"""Check if subscription exists.
|
|
652
|
+
|
|
653
|
+
Args:
|
|
654
|
+
subscription_type: Type of subscription
|
|
655
|
+
instrument: Instrument to check
|
|
656
|
+
|
|
657
|
+
Returns:
|
|
658
|
+
bool: True if subscription exists
|
|
659
|
+
"""
|
|
660
|
+
...
|
|
661
|
+
|
|
662
|
+
def get_base_subscription(self) -> str:
|
|
663
|
+
"""
|
|
664
|
+
Get the main subscription which should be used for the simulation.
|
|
665
|
+
This data is used for updating the internal OHLCV data series.
|
|
666
|
+
By default, simulation uses 1h OHLCV bars and live trading uses orderbook data.
|
|
667
|
+
"""
|
|
668
|
+
...
|
|
669
|
+
|
|
670
|
+
def set_base_subscription(self, subscription_type: str) -> None:
|
|
671
|
+
"""
|
|
672
|
+
Set the main subscription which should be used for the simulation.
|
|
673
|
+
|
|
674
|
+
Args:
|
|
675
|
+
subscription_type: Type of subscription (e.g. DataType.OHLC, DataType.OHLC["1h"])
|
|
676
|
+
"""
|
|
677
|
+
...
|
|
678
|
+
|
|
679
|
+
def get_subscriptions(self, instrument: Instrument | None = None) -> List[str]:
|
|
680
|
+
"""
|
|
681
|
+
Get all subscriptions for an instrument.
|
|
682
|
+
|
|
683
|
+
Args:
|
|
684
|
+
instrument: Instrument to get subscriptions for (optional)
|
|
685
|
+
|
|
686
|
+
Returns:
|
|
687
|
+
List[str]: List of subscriptions
|
|
688
|
+
"""
|
|
689
|
+
...
|
|
690
|
+
|
|
691
|
+
def get_subscribed_instruments(self, subscription_type: str | None = None) -> List[Instrument]:
|
|
692
|
+
"""
|
|
693
|
+
Get a list of instruments that are subscribed to a specific subscription type.
|
|
694
|
+
|
|
695
|
+
Args:
|
|
696
|
+
subscription_type: Type of subscription to filter by (optional)
|
|
697
|
+
|
|
698
|
+
Returns:
|
|
699
|
+
List[Instrument]: List of subscribed instruments
|
|
700
|
+
"""
|
|
701
|
+
...
|
|
702
|
+
|
|
703
|
+
def get_warmup(self, subscription_type: str) -> str | None:
|
|
704
|
+
"""
|
|
705
|
+
Get the warmup period for a subscription type.
|
|
706
|
+
|
|
707
|
+
Args:
|
|
708
|
+
subscription_type: Type of subscription (e.g. DataType.OHLC["1h"], etc.)
|
|
709
|
+
|
|
710
|
+
Returns:
|
|
711
|
+
str: Warmup period or None if no warmup period is set
|
|
712
|
+
"""
|
|
713
|
+
...
|
|
714
|
+
|
|
715
|
+
def set_warmup(self, configs: dict[Any, str]) -> None:
|
|
716
|
+
"""
|
|
717
|
+
Set the warmup period for different subscriptions.
|
|
718
|
+
|
|
719
|
+
If there are multiple ohlc configs specified, they will be warmed up in parallel.
|
|
720
|
+
|
|
721
|
+
Args:
|
|
722
|
+
configs: Dictionary of subscription types and warmup periods.
|
|
723
|
+
Keys can be subscription types of dictionaries with subscription parameters.
|
|
724
|
+
|
|
725
|
+
Example:
|
|
726
|
+
set_warmup({
|
|
727
|
+
DataType.OHLC["1h"]: "30d",
|
|
728
|
+
DataType.OHLC["1Min"]: "6h",
|
|
729
|
+
DataType.OHLC["1Sec"]: "5Min",
|
|
730
|
+
DataType.TRADE: "1h",
|
|
731
|
+
})
|
|
732
|
+
"""
|
|
733
|
+
...
|
|
734
|
+
|
|
735
|
+
def commit(self) -> None:
|
|
736
|
+
"""
|
|
737
|
+
Apply all pending changes.
|
|
738
|
+
"""
|
|
739
|
+
...
|
|
740
|
+
|
|
741
|
+
@property
|
|
742
|
+
def auto_subscribe(self) -> bool:
|
|
743
|
+
"""
|
|
744
|
+
Get whether new instruments are automatically subscribed to existing subscriptions.
|
|
745
|
+
|
|
746
|
+
Returns:
|
|
747
|
+
bool: True if auto-subscription is enabled
|
|
748
|
+
"""
|
|
749
|
+
...
|
|
750
|
+
|
|
751
|
+
@auto_subscribe.setter
|
|
752
|
+
def auto_subscribe(self, value: bool) -> None:
|
|
753
|
+
"""
|
|
754
|
+
Enable or disable automatic subscription of new instruments.
|
|
755
|
+
|
|
756
|
+
Args:
|
|
757
|
+
value: True to enable auto-subscription, False to disable
|
|
758
|
+
"""
|
|
759
|
+
...
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
class IAccountProcessor(IAccountViewer):
|
|
763
|
+
time_provider: ITimeProvider
|
|
764
|
+
|
|
765
|
+
def start(self):
|
|
766
|
+
"""
|
|
767
|
+
Start the account processor.
|
|
768
|
+
"""
|
|
769
|
+
...
|
|
770
|
+
|
|
771
|
+
def stop(self):
|
|
772
|
+
"""
|
|
773
|
+
Stop the account processor.
|
|
774
|
+
"""
|
|
775
|
+
...
|
|
776
|
+
|
|
777
|
+
def set_subscription_manager(self, manager: ISubscriptionManager) -> None:
|
|
778
|
+
"""Set the subscription manager for the account processor.
|
|
779
|
+
|
|
780
|
+
Args:
|
|
781
|
+
manager: ISubscriptionManager instance to set
|
|
782
|
+
"""
|
|
783
|
+
...
|
|
784
|
+
|
|
785
|
+
def update_balance(self, currency: str, total: float, locked: float):
|
|
786
|
+
"""Update balance for a specific currency.
|
|
787
|
+
|
|
788
|
+
Args:
|
|
789
|
+
currency: Currency code
|
|
790
|
+
total: Total amount of currency
|
|
791
|
+
locked: Amount of locked currency
|
|
792
|
+
"""
|
|
793
|
+
...
|
|
794
|
+
|
|
795
|
+
# TODO: refactor interface to accept float, Quote, Trade
|
|
796
|
+
def update_position_price(self, time: dt_64, instrument: Instrument, price: float) -> None:
|
|
797
|
+
"""Update position price for an instrument.
|
|
798
|
+
|
|
799
|
+
Args:
|
|
800
|
+
time: Timestamp of the update
|
|
801
|
+
instrument: Instrument being updated
|
|
802
|
+
price: New price
|
|
803
|
+
"""
|
|
804
|
+
...
|
|
805
|
+
|
|
806
|
+
def process_deals(self, instrument: Instrument, deals: list[Deal]) -> None:
|
|
807
|
+
"""Process executed deals for an instrument.
|
|
808
|
+
|
|
809
|
+
Args:
|
|
810
|
+
instrument: Instrument the deals belong to
|
|
811
|
+
deals: List of deals to process
|
|
812
|
+
"""
|
|
813
|
+
...
|
|
814
|
+
|
|
815
|
+
def process_order(self, order: Order) -> None:
|
|
816
|
+
"""Process order updates.
|
|
817
|
+
|
|
818
|
+
Args:
|
|
819
|
+
order: Order to process
|
|
820
|
+
"""
|
|
821
|
+
...
|
|
822
|
+
|
|
823
|
+
def attach_positions(self, *position: Position) -> "IAccountProcessor":
|
|
824
|
+
"""Attach positions to the account.
|
|
825
|
+
|
|
826
|
+
Args:
|
|
827
|
+
*position: Position objects to attach
|
|
828
|
+
|
|
829
|
+
Returns:
|
|
830
|
+
I"IAccountProcessor": Self for chaining
|
|
831
|
+
"""
|
|
832
|
+
...
|
|
833
|
+
|
|
834
|
+
def add_active_orders(self, orders: Dict[str, Order]) -> None:
|
|
835
|
+
"""Add active orders to the account.
|
|
836
|
+
|
|
837
|
+
Warning only use in the beginning for state restoration because it does not update locked balances.
|
|
838
|
+
|
|
839
|
+
Args:
|
|
840
|
+
orders: Dictionary mapping order IDs to Order objects
|
|
841
|
+
"""
|
|
842
|
+
...
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
class IProcessingManager:
|
|
846
|
+
"""Manages event processing."""
|
|
847
|
+
|
|
848
|
+
def process_data(self, instrument: Instrument, d_type: str, data: Any, is_historical: bool) -> bool:
|
|
849
|
+
"""
|
|
850
|
+
Process incoming data.
|
|
851
|
+
|
|
852
|
+
Args:
|
|
853
|
+
instrument: Instrument the data is for
|
|
854
|
+
d_type: Type of the data
|
|
855
|
+
data: The data to process
|
|
856
|
+
|
|
857
|
+
Returns:
|
|
858
|
+
bool: True if processing should be halted
|
|
859
|
+
"""
|
|
860
|
+
...
|
|
861
|
+
|
|
862
|
+
def set_fit_schedule(self, schedule: str) -> None:
|
|
863
|
+
"""
|
|
864
|
+
Set the schedule for fitting the strategy model (default is to trigger fit only at start).
|
|
865
|
+
"""
|
|
866
|
+
...
|
|
867
|
+
|
|
868
|
+
def set_event_schedule(self, schedule: str) -> None:
|
|
869
|
+
"""
|
|
870
|
+
Set the schedule for triggering events (default is to only trigger on data events).
|
|
871
|
+
"""
|
|
872
|
+
...
|
|
873
|
+
|
|
874
|
+
def get_event_schedule(self, event_id: str) -> str | None:
|
|
875
|
+
"""
|
|
876
|
+
Get defined schedule for event id.
|
|
877
|
+
"""
|
|
878
|
+
...
|
|
879
|
+
|
|
880
|
+
def is_fitted(self) -> bool:
|
|
881
|
+
"""
|
|
882
|
+
Check if the strategy is fitted.
|
|
883
|
+
"""
|
|
884
|
+
...
|
|
885
|
+
|
|
886
|
+
|
|
887
|
+
class IStrategyContext(
|
|
888
|
+
IMarketManager,
|
|
889
|
+
ITradingManager,
|
|
890
|
+
IUniverseManager,
|
|
891
|
+
ISubscriptionManager,
|
|
892
|
+
IProcessingManager,
|
|
893
|
+
IAccountViewer,
|
|
894
|
+
):
|
|
895
|
+
strategy: "IStrategy"
|
|
896
|
+
|
|
897
|
+
def start(self, blocking: bool = False):
|
|
898
|
+
"""
|
|
899
|
+
Starts the strategy context.
|
|
900
|
+
|
|
901
|
+
Args:
|
|
902
|
+
blocking: Whether to block the main thread
|
|
903
|
+
"""
|
|
904
|
+
...
|
|
905
|
+
|
|
906
|
+
def stop(self):
|
|
907
|
+
"""Stops the strategy context."""
|
|
908
|
+
...
|
|
909
|
+
|
|
910
|
+
def is_running(self) -> bool:
|
|
911
|
+
"""
|
|
912
|
+
Check if the strategy is running.
|
|
913
|
+
"""
|
|
914
|
+
...
|
|
915
|
+
|
|
916
|
+
@property
|
|
917
|
+
def is_simulation(self) -> bool:
|
|
918
|
+
"""
|
|
919
|
+
Check if the strategy is running in simulation mode.
|
|
920
|
+
"""
|
|
921
|
+
...
|
|
922
|
+
|
|
923
|
+
@property
|
|
924
|
+
def exchanges(self) -> list[str]:
|
|
925
|
+
"""
|
|
926
|
+
Returns a list of exchanges in this context. There is one exchange in the most cases.
|
|
927
|
+
"""
|
|
928
|
+
...
|
|
929
|
+
|
|
930
|
+
|
|
931
|
+
class IPositionGathering:
|
|
932
|
+
"""
|
|
933
|
+
Common interface for position gathering
|
|
934
|
+
"""
|
|
935
|
+
|
|
936
|
+
def alter_position_size(self, ctx: IStrategyContext, target: TargetPosition) -> float: ...
|
|
937
|
+
|
|
938
|
+
def alter_positions(
|
|
939
|
+
self, ctx: IStrategyContext, targets: List[TargetPosition] | TargetPosition
|
|
940
|
+
) -> Dict[Instrument, float]:
|
|
941
|
+
if not isinstance(targets, list):
|
|
942
|
+
targets = [targets]
|
|
943
|
+
|
|
944
|
+
res = {}
|
|
945
|
+
if targets:
|
|
946
|
+
for t in targets:
|
|
947
|
+
if t.is_service: # we skip processing service positions
|
|
948
|
+
continue
|
|
949
|
+
try:
|
|
950
|
+
res[t.instrument] = self.alter_position_size(ctx, t)
|
|
951
|
+
except Exception as ex:
|
|
952
|
+
logger.error(f"[{ctx.time()}]: Failed processing target position {t} : {ex}")
|
|
953
|
+
logger.opt(colors=False).error(traceback.format_exc())
|
|
954
|
+
return res
|
|
955
|
+
|
|
956
|
+
def on_execution_report(self, ctx: IStrategyContext, instrument: Instrument, deal: Deal): ...
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
class IPositionSizer:
|
|
960
|
+
"""Interface for calculating target positions from signals."""
|
|
961
|
+
|
|
962
|
+
def calculate_target_positions(self, ctx: IStrategyContext, signals: list[Signal]) -> list[TargetPosition]:
|
|
963
|
+
"""Calculates target position sizes.
|
|
964
|
+
|
|
965
|
+
Args:
|
|
966
|
+
ctx: Strategy context object.
|
|
967
|
+
signals: List of signals to process.
|
|
968
|
+
|
|
969
|
+
Returns:
|
|
970
|
+
List of target positions.
|
|
971
|
+
"""
|
|
972
|
+
raise NotImplementedError("calculate_target_positions is not implemented")
|
|
973
|
+
|
|
974
|
+
def get_signal_entry_price(
|
|
975
|
+
self, ctx: IStrategyContext, signal: Signal, use_mid_price: bool = False
|
|
976
|
+
) -> float | None:
|
|
977
|
+
"""
|
|
978
|
+
Get the entry price for a signal.
|
|
979
|
+
"""
|
|
980
|
+
_entry = None
|
|
981
|
+
if signal.price is not None and signal.price > 0:
|
|
982
|
+
_entry = signal.price
|
|
983
|
+
else:
|
|
984
|
+
if (_q := ctx.quote(signal.instrument)) is not None:
|
|
985
|
+
_entry = _q.mid_price() if use_mid_price else (_q.ask if np.sign(signal.signal) > 0 else _q.bid)
|
|
986
|
+
else:
|
|
987
|
+
logger.error(
|
|
988
|
+
f"{self.__class__.__name__}: Can't get actual market quote for {signal.instrument} and signal price is not set ({str(signal)}) !"
|
|
989
|
+
)
|
|
990
|
+
|
|
991
|
+
return _entry
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
class PositionsTracker:
|
|
995
|
+
"""
|
|
996
|
+
Process signals from strategy and track position. It can contains logic for risk management for example.
|
|
997
|
+
"""
|
|
998
|
+
|
|
999
|
+
_sizer: IPositionSizer
|
|
1000
|
+
|
|
1001
|
+
def __init__(self, sizer: IPositionSizer) -> None:
|
|
1002
|
+
self._sizer = sizer
|
|
1003
|
+
|
|
1004
|
+
def get_position_sizer(self) -> IPositionSizer:
|
|
1005
|
+
return self._sizer
|
|
1006
|
+
|
|
1007
|
+
def is_active(self, instrument: Instrument) -> bool:
|
|
1008
|
+
return True
|
|
1009
|
+
|
|
1010
|
+
def process_signals(self, ctx: IStrategyContext, signals: list[Signal]) -> list[TargetPosition] | TargetPosition:
|
|
1011
|
+
"""
|
|
1012
|
+
Default implementation just returns calculated target positions
|
|
1013
|
+
"""
|
|
1014
|
+
return self.get_position_sizer().calculate_target_positions(ctx, signals)
|
|
1015
|
+
|
|
1016
|
+
def update(
|
|
1017
|
+
self, ctx: IStrategyContext, instrument: Instrument, update: Timestamped
|
|
1018
|
+
) -> List[TargetPosition] | TargetPosition:
|
|
1019
|
+
"""
|
|
1020
|
+
Tracker is being updated by new market data.
|
|
1021
|
+
It may require to change position size or create new position because of interior tracker's logic (risk management for example).
|
|
1022
|
+
"""
|
|
1023
|
+
...
|
|
1024
|
+
|
|
1025
|
+
def on_execution_report(self, ctx: IStrategyContext, instrument: Instrument, deal: Deal):
|
|
1026
|
+
"""
|
|
1027
|
+
Tracker is notified when execution report is received
|
|
1028
|
+
"""
|
|
1029
|
+
...
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
def _unpickle_instance(chain: tuple[type], state: dict):
|
|
1033
|
+
"""
|
|
1034
|
+
chain is a tuple of the *original* classes, e.g. (A, B, C).
|
|
1035
|
+
Reconstruct a new ephemeral class that inherits from them.
|
|
1036
|
+
"""
|
|
1037
|
+
name = "_".join(cls.__name__ for cls in chain)
|
|
1038
|
+
# Reverse the chain to respect the typical left-to-right MRO
|
|
1039
|
+
inst = type(name, chain[::-1], {"__module__": "__main__"})()
|
|
1040
|
+
inst.__dict__.update(state)
|
|
1041
|
+
return inst
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
class Mixable(type):
|
|
1045
|
+
"""
|
|
1046
|
+
It's possible to create composite strategies dynamically by adding mixins with functionality.
|
|
1047
|
+
|
|
1048
|
+
NewStrategy = (SignalGenerator + RiskManager + PositionGathering)
|
|
1049
|
+
NewStrategy(....) can be used in simulation or live trading.
|
|
1050
|
+
"""
|
|
1051
|
+
|
|
1052
|
+
def __add__(cls, other_cls):
|
|
1053
|
+
# If we already have a _composition, combine them;
|
|
1054
|
+
# else treat cls itself as the start of the chain
|
|
1055
|
+
cls_chain = getattr(cls, "__composition__", (cls,))
|
|
1056
|
+
other_chain = getattr(other_cls, "__composition__", (other_cls,))
|
|
1057
|
+
|
|
1058
|
+
# Combine them into one chain. You can define your own order rules:
|
|
1059
|
+
new_chain = cls_chain + other_chain
|
|
1060
|
+
|
|
1061
|
+
# Create ephemeral class
|
|
1062
|
+
name = "_".join(c.__name__ for c in new_chain)
|
|
1063
|
+
|
|
1064
|
+
def __reduce__(self):
|
|
1065
|
+
# Just return the chain of *original real classes*
|
|
1066
|
+
return _unpickle_instance, (new_chain, self.__dict__)
|
|
1067
|
+
|
|
1068
|
+
new_cls = type(
|
|
1069
|
+
name,
|
|
1070
|
+
new_chain[::-1],
|
|
1071
|
+
{"__module__": cls.__module__, "__composition__": new_chain, "__reduce__": __reduce__},
|
|
1072
|
+
)
|
|
1073
|
+
return new_cls
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
class IStrategy(metaclass=Mixable):
|
|
1077
|
+
"""Base class for trading strategies."""
|
|
1078
|
+
|
|
1079
|
+
ctx: IStrategyContext
|
|
1080
|
+
|
|
1081
|
+
def __init__(self, **kwargs) -> None:
|
|
1082
|
+
set_parameters_to_object(self, **kwargs)
|
|
1083
|
+
|
|
1084
|
+
def on_init(self, ctx: IStrategyContext):
|
|
1085
|
+
"""
|
|
1086
|
+
This method is called when strategy is initialized.
|
|
1087
|
+
It is useful for setting the base subscription and warmup periods via the subscription manager.
|
|
1088
|
+
"""
|
|
1089
|
+
...
|
|
1090
|
+
|
|
1091
|
+
def on_start(self, ctx: IStrategyContext):
|
|
1092
|
+
"""
|
|
1093
|
+
This method is called strategy is started. You can already use the market data provider.
|
|
1094
|
+
"""
|
|
1095
|
+
pass
|
|
1096
|
+
|
|
1097
|
+
def on_fit(self, ctx: IStrategyContext):
|
|
1098
|
+
"""
|
|
1099
|
+
Called when it's time to fit the model.
|
|
1100
|
+
"""
|
|
1101
|
+
return None
|
|
1102
|
+
|
|
1103
|
+
def on_universe_change(
|
|
1104
|
+
self, ctx: IStrategyContext, add_instruments: list[Instrument], rm_instruments: list[Instrument]
|
|
1105
|
+
) -> None:
|
|
1106
|
+
"""
|
|
1107
|
+
This method is called when the trading universe is updated.
|
|
1108
|
+
"""
|
|
1109
|
+
return None
|
|
1110
|
+
|
|
1111
|
+
def on_event(self, ctx: IStrategyContext, event: TriggerEvent) -> List[Signal] | Signal | None:
|
|
1112
|
+
"""Called on strategy events.
|
|
1113
|
+
|
|
1114
|
+
Args:
|
|
1115
|
+
ctx: Strategy context.
|
|
1116
|
+
event: Trigger event to process.
|
|
1117
|
+
|
|
1118
|
+
Returns:
|
|
1119
|
+
List of signals, single signal, or None.
|
|
1120
|
+
"""
|
|
1121
|
+
return None
|
|
1122
|
+
|
|
1123
|
+
def on_market_data(self, ctx: IStrategyContext, data: MarketEvent) -> List[Signal] | Signal | None:
|
|
1124
|
+
"""
|
|
1125
|
+
Called when new market data is received.
|
|
1126
|
+
|
|
1127
|
+
Args:
|
|
1128
|
+
ctx: Strategy context.
|
|
1129
|
+
data: The market data received.
|
|
1130
|
+
|
|
1131
|
+
Returns:
|
|
1132
|
+
List of signals, single signal, or None.
|
|
1133
|
+
"""
|
|
1134
|
+
return None
|
|
1135
|
+
|
|
1136
|
+
def on_order_update(self, ctx: IStrategyContext, order: Order) -> list[Signal] | Signal | None:
|
|
1137
|
+
"""
|
|
1138
|
+
Called when an order update is received.
|
|
1139
|
+
|
|
1140
|
+
Args:
|
|
1141
|
+
ctx: Strategy context.
|
|
1142
|
+
order: The order update.
|
|
1143
|
+
"""
|
|
1144
|
+
return None
|
|
1145
|
+
|
|
1146
|
+
def on_stop(self, ctx: IStrategyContext):
|
|
1147
|
+
pass
|
|
1148
|
+
|
|
1149
|
+
def tracker(self, ctx: IStrategyContext) -> PositionsTracker | None:
|
|
1150
|
+
pass
|