Qubx 0.6.59__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.60__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/backtester/simulator.py +7 -6
- qubx/backtester/utils.py +1 -1
- qubx/core/basics.py +131 -60
- qubx/core/context.py +17 -0
- qubx/core/interfaces.py +43 -3
- qubx/core/loggers.py +64 -36
- qubx/core/metrics.py +20 -9
- qubx/core/mixins/processing.py +177 -80
- qubx/core/mixins/universe.py +12 -5
- qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/emitters/__init__.py +9 -1
- qubx/emitters/indicator.py +213 -0
- qubx/gathering/simplest.py +1 -1
- qubx/loggers/csv.py +22 -7
- qubx/loggers/inmemory.py +18 -6
- qubx/loggers/mongo.py +2 -1
- qubx/restarts/state_resolvers.py +62 -25
- qubx/restarts/time_finders.py +47 -4
- qubx/restorers/interfaces.py +8 -2
- qubx/restorers/signal.py +209 -126
- qubx/restorers/state.py +25 -9
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/trackers/advanced.py +4 -5
- qubx/trackers/composite.py +4 -4
- qubx/trackers/riskctrl.py +151 -37
- qubx/trackers/sizers.py +8 -8
- qubx/utils/runner/_jupyter_runner.pyt +1 -1
- qubx/utils/runner/runner.py +3 -2
- {qubx-0.6.59.dist-info → qubx-0.6.60.dist-info}/METADATA +1 -1
- {qubx-0.6.59.dist-info → qubx-0.6.60.dist-info}/RECORD +34 -33
- {qubx-0.6.59.dist-info → qubx-0.6.60.dist-info}/LICENSE +0 -0
- {qubx-0.6.59.dist-info → qubx-0.6.60.dist-info}/WHEEL +0 -0
- {qubx-0.6.59.dist-info → qubx-0.6.60.dist-info}/entry_points.txt +0 -0
qubx/backtester/simulator.py
CHANGED
|
@@ -265,12 +265,13 @@ def _run_setup(
|
|
|
265
265
|
stop,
|
|
266
266
|
setup.exchanges,
|
|
267
267
|
setup.instruments,
|
|
268
|
-
setup.capital,
|
|
269
|
-
setup.base_currency,
|
|
270
|
-
commissions_for_result,
|
|
271
|
-
runner.logs_writer.get_portfolio(as_plain_dataframe=True),
|
|
272
|
-
runner.logs_writer.get_executions(),
|
|
273
|
-
runner.logs_writer.get_signals(),
|
|
268
|
+
capital=setup.capital,
|
|
269
|
+
base_currency=setup.base_currency,
|
|
270
|
+
commissions=commissions_for_result,
|
|
271
|
+
portfolio_log=runner.logs_writer.get_portfolio(as_plain_dataframe=True),
|
|
272
|
+
executions_log=runner.logs_writer.get_executions(),
|
|
273
|
+
signals_log=runner.logs_writer.get_signals(),
|
|
274
|
+
targets_log=runner.logs_writer.get_targets(),
|
|
274
275
|
strategy_class=runner.strategy_class,
|
|
275
276
|
parameters=runner.strategy_params,
|
|
276
277
|
is_simulation=True,
|
qubx/backtester/utils.py
CHANGED
|
@@ -209,7 +209,7 @@ class SignalsProxy(IStrategy):
|
|
|
209
209
|
signal = event.data.get("order")
|
|
210
210
|
# - TODO: also need to think about how to pass stop/take here
|
|
211
211
|
if signal is not None and event.instrument:
|
|
212
|
-
return [event.instrument.signal(signal)]
|
|
212
|
+
return [event.instrument.signal(ctx, signal)]
|
|
213
213
|
return None
|
|
214
214
|
|
|
215
215
|
|
qubx/core/basics.py
CHANGED
|
@@ -56,22 +56,73 @@ class TimestampedDict:
|
|
|
56
56
|
data: dict[str, Any]
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
class ITimeProvider:
|
|
60
|
+
"""
|
|
61
|
+
Generic interface for providing current time
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def time(self) -> dt_64:
|
|
65
|
+
"""
|
|
66
|
+
Returns current time
|
|
67
|
+
"""
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
|
|
59
71
|
# Alias for timestamped data types used in Qubx
|
|
60
72
|
Timestamped: TypeAlias = Quote | Trade | Bar | OrderBook | TimestampedDict | FundingRate | Liquidation
|
|
61
73
|
|
|
62
74
|
|
|
75
|
+
@dataclass
|
|
76
|
+
class TargetPosition:
|
|
77
|
+
"""
|
|
78
|
+
Class for presenting target position calculated from signal
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
time: dt_64 | str # time when position was created
|
|
82
|
+
instrument: "Instrument"
|
|
83
|
+
target_position_size: float # actual position size after processing in sizer
|
|
84
|
+
entry_price: float | None = None
|
|
85
|
+
stop_price: float | None = None
|
|
86
|
+
take_price: float | None = None
|
|
87
|
+
options: dict[str, Any] = field(default_factory=dict)
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def price(self) -> float | None:
|
|
91
|
+
return self.entry_price
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def stop(self) -> float | None:
|
|
95
|
+
return self.stop_price
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def take(self) -> float | None:
|
|
99
|
+
return self.take_price
|
|
100
|
+
|
|
101
|
+
def __str__(self) -> str:
|
|
102
|
+
_d = f"{pd.Timestamp(self.time).strftime('%Y-%m-%d %H:%M:%S.%f')}"
|
|
103
|
+
_p = f" @ {self.entry_price}" if self.entry_price is not None else ""
|
|
104
|
+
_s = f" stop: {self.stop_price}" if self.stop_price is not None else ""
|
|
105
|
+
_t = f" take: {self.take_price}" if self.take_price is not None else ""
|
|
106
|
+
return f"[{_d}] TARGET {self.target_position_size:+f} {self.instrument.base}{_p}{_s}{_t} for {self.instrument}"
|
|
107
|
+
|
|
108
|
+
|
|
63
109
|
@dataclass
|
|
64
110
|
class Signal:
|
|
65
111
|
"""
|
|
66
112
|
Class for presenting signals generated by strategy
|
|
67
113
|
|
|
68
114
|
Attributes:
|
|
69
|
-
reference_price: float -
|
|
115
|
+
reference_price: float - aux market price when signal was generated
|
|
116
|
+
is_service: bool - when we need this signal only for informative purposes (post-factum risk management etc)
|
|
70
117
|
|
|
71
118
|
Options:
|
|
72
|
-
|
|
119
|
+
- allow_override: bool - if True, and there is another signal for the same instrument, then override current.
|
|
120
|
+
- group: str - group name for signal
|
|
121
|
+
- comment: str - comment for signal
|
|
122
|
+
- options: dict[str, Any] - additional options for signal
|
|
73
123
|
"""
|
|
74
124
|
|
|
125
|
+
time: dt_64 | str # time when signal was generated
|
|
75
126
|
instrument: "Instrument"
|
|
76
127
|
signal: float
|
|
77
128
|
price: float | None = None
|
|
@@ -81,20 +132,37 @@ class Signal:
|
|
|
81
132
|
group: str = ""
|
|
82
133
|
comment: str = ""
|
|
83
134
|
options: dict[str, Any] = field(default_factory=dict)
|
|
135
|
+
is_service: bool = False # when we need this signal only for informative purposes (post-factum risk management etc)
|
|
136
|
+
|
|
137
|
+
def target_for_amount(self, amount: float, **kwargs) -> TargetPosition:
|
|
138
|
+
assert not self.is_service, "Service signals can't be converted to target positions !"
|
|
139
|
+
return self.instrument.target(
|
|
140
|
+
self.time,
|
|
141
|
+
self.instrument.round_size_down(amount),
|
|
142
|
+
entry_price=self.price,
|
|
143
|
+
stop_price=self.stop,
|
|
144
|
+
take_price=self.take,
|
|
145
|
+
options=self.options,
|
|
146
|
+
**kwargs,
|
|
147
|
+
)
|
|
84
148
|
|
|
85
149
|
def __str__(self) -> str:
|
|
150
|
+
_d = f"{pd.Timestamp(self.time).strftime('%Y-%m-%d %H:%M:%S.%f')}"
|
|
86
151
|
_p = f" @ {self.price}" if self.price is not None else ""
|
|
87
152
|
_s = f" stop: {self.stop}" if self.stop is not None else ""
|
|
88
153
|
_t = f" take: {self.take}" if self.take is not None else ""
|
|
89
154
|
_r = f" {self.reference_price:.2f}" if self.reference_price is not None else ""
|
|
90
155
|
_c = f" ({self.comment})" if self.comment else ""
|
|
91
|
-
|
|
156
|
+
_i = "SERVICE ::" if self.is_service else ""
|
|
157
|
+
|
|
158
|
+
return f"[{_d}] {_i}{self.group}{_r} {self.signal:+.2f} {self.instrument}{_p}{_s}{_t}{_c}"
|
|
92
159
|
|
|
93
160
|
def copy(self) -> "Signal":
|
|
94
161
|
"""
|
|
95
162
|
Return a copy of the original signal
|
|
96
163
|
"""
|
|
97
164
|
return Signal(
|
|
165
|
+
self.time,
|
|
98
166
|
self.instrument,
|
|
99
167
|
self.signal,
|
|
100
168
|
self.price,
|
|
@@ -104,60 +172,27 @@ class Signal:
|
|
|
104
172
|
self.group,
|
|
105
173
|
self.comment,
|
|
106
174
|
dict(self.options),
|
|
175
|
+
self.is_service,
|
|
107
176
|
)
|
|
108
177
|
|
|
109
178
|
|
|
110
179
|
@dataclass
|
|
111
|
-
class
|
|
180
|
+
class InitializingSignal(Signal):
|
|
112
181
|
"""
|
|
113
|
-
|
|
182
|
+
Special signal type for post-warmup initialization
|
|
114
183
|
"""
|
|
115
184
|
|
|
116
|
-
|
|
117
|
-
signal: Signal # original signal
|
|
118
|
-
target_position_size: float # actual position size after processing in sizer
|
|
119
|
-
_is_service: bool = False
|
|
120
|
-
|
|
121
|
-
@staticmethod
|
|
122
|
-
def create(ctx: "ITimeProvider", signal: Signal, target_size: float) -> "TargetPosition":
|
|
123
|
-
return TargetPosition(ctx.time(), signal, signal.instrument.round_size_down(target_size))
|
|
124
|
-
|
|
125
|
-
@staticmethod
|
|
126
|
-
def zero(ctx: "ITimeProvider", signal: Signal) -> "TargetPosition":
|
|
127
|
-
return TargetPosition(ctx.time(), signal, 0.0)
|
|
128
|
-
|
|
129
|
-
@staticmethod
|
|
130
|
-
def service(ctx: "ITimeProvider", signal: Signal, size: float | None = None) -> "TargetPosition":
|
|
131
|
-
"""
|
|
132
|
-
Generate just service position target (for logging purposes)
|
|
133
|
-
"""
|
|
134
|
-
return TargetPosition(ctx.time(), signal, size if size else signal.signal, _is_service=True)
|
|
135
|
-
|
|
136
|
-
@property
|
|
137
|
-
def instrument(self) -> "Instrument":
|
|
138
|
-
return self.signal.instrument
|
|
139
|
-
|
|
140
|
-
@property
|
|
141
|
-
def price(self) -> float | None:
|
|
142
|
-
return self.signal.price
|
|
143
|
-
|
|
144
|
-
@property
|
|
145
|
-
def stop(self) -> float | None:
|
|
146
|
-
return self.signal.stop
|
|
147
|
-
|
|
148
|
-
@property
|
|
149
|
-
def take(self) -> float | None:
|
|
150
|
-
return self.signal.take
|
|
151
|
-
|
|
152
|
-
@property
|
|
153
|
-
def is_service(self) -> bool:
|
|
154
|
-
"""
|
|
155
|
-
Some target may be used just for informative purposes (post-factum risk management etc)
|
|
156
|
-
"""
|
|
157
|
-
return self._is_service
|
|
185
|
+
use_limit_order: bool = False # if True, then use limit order for post-warmup initialization
|
|
158
186
|
|
|
159
187
|
def __str__(self) -> str:
|
|
160
|
-
|
|
188
|
+
_d = f"{pd.Timestamp(self.time).strftime('%Y-%m-%d %H:%M:%S.%f')}"
|
|
189
|
+
_p = f" @ {self.price}" if self.price is not None else ""
|
|
190
|
+
_s = f" stop: {self.stop}" if self.stop is not None else ""
|
|
191
|
+
_t = f" take: {self.take}" if self.take is not None else ""
|
|
192
|
+
_r = f" {self.reference_price:.2f}" if self.reference_price is not None else ""
|
|
193
|
+
_c = f" ({self.comment})" if self.comment else ""
|
|
194
|
+
|
|
195
|
+
return f"[{_d}] POST-WARMUP-INIT ::{self.group}{_r} {self.signal:+.2f} {self.instrument}{_p}{_s}{_t}{_c}"
|
|
161
196
|
|
|
162
197
|
|
|
163
198
|
class AssetType(StrEnum):
|
|
@@ -261,8 +296,26 @@ class Instrument:
|
|
|
261
296
|
"""
|
|
262
297
|
return prec_ceil(price, self.price_precision)
|
|
263
298
|
|
|
299
|
+
def service_signal(
|
|
300
|
+
self,
|
|
301
|
+
time: dt_64 | str | ITimeProvider,
|
|
302
|
+
signal: float,
|
|
303
|
+
price: float | None = None,
|
|
304
|
+
stop: float | None = None,
|
|
305
|
+
take: float | None = None,
|
|
306
|
+
group: str = "",
|
|
307
|
+
comment: str = "",
|
|
308
|
+
options: dict[str, Any] | None = None,
|
|
309
|
+
**kwargs,
|
|
310
|
+
) -> Signal:
|
|
311
|
+
"""
|
|
312
|
+
Create service signal for the instrument
|
|
313
|
+
"""
|
|
314
|
+
return self.signal(time, signal, price, stop, take, group, comment, options, is_service=True, **kwargs)
|
|
315
|
+
|
|
264
316
|
def signal(
|
|
265
317
|
self,
|
|
318
|
+
time: dt_64 | str | ITimeProvider,
|
|
266
319
|
signal: float,
|
|
267
320
|
price: float | None = None,
|
|
268
321
|
stop: float | None = None,
|
|
@@ -270,9 +323,14 @@ class Instrument:
|
|
|
270
323
|
group: str = "",
|
|
271
324
|
comment: str = "",
|
|
272
325
|
options: dict[str, Any] | None = None,
|
|
326
|
+
is_service: bool = False,
|
|
273
327
|
**kwargs,
|
|
274
328
|
) -> Signal:
|
|
329
|
+
"""
|
|
330
|
+
Create signal for the instrument
|
|
331
|
+
"""
|
|
275
332
|
return Signal(
|
|
333
|
+
time=time.time() if isinstance(time, ITimeProvider) else time,
|
|
276
334
|
instrument=self,
|
|
277
335
|
signal=signal,
|
|
278
336
|
price=price,
|
|
@@ -281,6 +339,30 @@ class Instrument:
|
|
|
281
339
|
group=group,
|
|
282
340
|
comment=comment,
|
|
283
341
|
options=(options or {}) | kwargs,
|
|
342
|
+
is_service=is_service,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
def target(
|
|
346
|
+
self,
|
|
347
|
+
time: dt_64 | str | ITimeProvider,
|
|
348
|
+
amount: float,
|
|
349
|
+
entry_price: float | None = None,
|
|
350
|
+
stop_price: float | None = None,
|
|
351
|
+
take_price: float | None = None,
|
|
352
|
+
options: dict[str, Any] | None = None,
|
|
353
|
+
**kwargs,
|
|
354
|
+
) -> TargetPosition:
|
|
355
|
+
"""
|
|
356
|
+
Create target position for the instrument
|
|
357
|
+
"""
|
|
358
|
+
return TargetPosition(
|
|
359
|
+
time=time.time() if isinstance(time, ITimeProvider) else time,
|
|
360
|
+
instrument=self,
|
|
361
|
+
target_position_size=self.round_size_down(amount),
|
|
362
|
+
entry_price=entry_price,
|
|
363
|
+
stop_price=stop_price,
|
|
364
|
+
take_price=take_price,
|
|
365
|
+
options=(options or {}) | kwargs,
|
|
284
366
|
)
|
|
285
367
|
|
|
286
368
|
def __hash__(self) -> int:
|
|
@@ -736,18 +818,6 @@ class CtrlChannel:
|
|
|
736
818
|
raise QueueTimeout(f"Timeout waiting for data on {self.name} channel")
|
|
737
819
|
|
|
738
820
|
|
|
739
|
-
class ITimeProvider:
|
|
740
|
-
"""
|
|
741
|
-
Generic interface for providing current time
|
|
742
|
-
"""
|
|
743
|
-
|
|
744
|
-
def time(self) -> dt_64:
|
|
745
|
-
"""
|
|
746
|
-
Returns current time
|
|
747
|
-
"""
|
|
748
|
-
...
|
|
749
|
-
|
|
750
|
-
|
|
751
821
|
class DataType(StrEnum):
|
|
752
822
|
"""
|
|
753
823
|
Data type constants. Used for specifying the type of data and can be used for subscription to.
|
|
@@ -884,6 +954,7 @@ class RestoredState:
|
|
|
884
954
|
|
|
885
955
|
time: np.datetime64
|
|
886
956
|
balances: dict[str, AssetBalance]
|
|
957
|
+
instrument_to_signal_positions: dict[Instrument, list[Signal]]
|
|
887
958
|
instrument_to_target_positions: dict[Instrument, list[TargetPosition]]
|
|
888
959
|
positions: dict[Instrument, Position]
|
|
889
960
|
|
qubx/core/context.py
CHANGED
|
@@ -15,6 +15,8 @@ from qubx.core.basics import (
|
|
|
15
15
|
Order,
|
|
16
16
|
OrderRequest,
|
|
17
17
|
Position,
|
|
18
|
+
Signal,
|
|
19
|
+
TargetPosition,
|
|
18
20
|
Timestamped,
|
|
19
21
|
dt_64,
|
|
20
22
|
)
|
|
@@ -87,6 +89,7 @@ class StrategyContext(IStrategyContext):
|
|
|
87
89
|
|
|
88
90
|
_warmup_positions: dict[Instrument, Position] | None = None
|
|
89
91
|
_warmup_orders: dict[Instrument, list[Order]] | None = None
|
|
92
|
+
_warmup_active_targets: dict[Instrument, list[TargetPosition]] | None = None
|
|
90
93
|
|
|
91
94
|
def __init__(
|
|
92
95
|
self,
|
|
@@ -420,6 +423,8 @@ class StrategyContext(IStrategyContext):
|
|
|
420
423
|
|
|
421
424
|
# ITradingManager delegation
|
|
422
425
|
def trade(self, instrument: Instrument, amount: float, price: float | None = None, time_in_force="gtc", **options):
|
|
426
|
+
# TODO: we need to generate target position and apply it in the processing manager
|
|
427
|
+
# - one of the options is to have multiple entry levels in TargetPosition class
|
|
423
428
|
return self._trading_manager.trade(instrument, amount, price, time_in_force, **options)
|
|
424
429
|
|
|
425
430
|
def trade_async(
|
|
@@ -522,16 +527,28 @@ class StrategyContext(IStrategyContext):
|
|
|
522
527
|
def is_fitted(self) -> bool:
|
|
523
528
|
return self._processing_manager.is_fitted()
|
|
524
529
|
|
|
530
|
+
def get_active_targets(self) -> dict[Instrument, list[TargetPosition]]:
|
|
531
|
+
return self._processing_manager.get_active_targets()
|
|
532
|
+
|
|
533
|
+
def emit_signal(self, signal: Signal) -> None:
|
|
534
|
+
return self._processing_manager.emit_signal(signal)
|
|
535
|
+
|
|
525
536
|
# IWarmupStateSaver delegation
|
|
526
537
|
def set_warmup_positions(self, positions: dict[Instrument, Position]) -> None:
|
|
527
538
|
self._warmup_positions = positions
|
|
528
539
|
|
|
540
|
+
def set_warmup_active_targets(self, active_targets: dict[Instrument, list[TargetPosition]]) -> None:
|
|
541
|
+
self._warmup_active_targets = active_targets
|
|
542
|
+
|
|
529
543
|
def set_warmup_orders(self, orders: dict[Instrument, list[Order]]) -> None:
|
|
530
544
|
self._warmup_orders = orders
|
|
531
545
|
|
|
532
546
|
def get_warmup_positions(self) -> dict[Instrument, Position]:
|
|
533
547
|
return self._warmup_positions if self._warmup_positions is not None else {}
|
|
534
548
|
|
|
549
|
+
def get_warmup_active_targets(self) -> dict[Instrument, list[TargetPosition]]:
|
|
550
|
+
return self._warmup_active_targets if self._warmup_active_targets is not None else {}
|
|
551
|
+
|
|
535
552
|
def get_warmup_orders(self) -> dict[Instrument, list[Order]]:
|
|
536
553
|
return self._warmup_orders if self._warmup_orders is not None else {}
|
|
537
554
|
|
qubx/core/interfaces.py
CHANGED
|
@@ -508,7 +508,9 @@ class IMarketManager(ITimeProvider):
|
|
|
508
508
|
"""
|
|
509
509
|
...
|
|
510
510
|
|
|
511
|
-
def ohlc_pd(
|
|
511
|
+
def ohlc_pd(
|
|
512
|
+
self, instrument: Instrument, timeframe: str | None = None, length: int | None = None, consolidated: bool = True
|
|
513
|
+
) -> pd.DataFrame:
|
|
512
514
|
"""Get OHLCV data for an instrument as pandas DataFrame.
|
|
513
515
|
|
|
514
516
|
Args:
|
|
@@ -1035,6 +1037,26 @@ class IProcessingManager:
|
|
|
1035
1037
|
"""
|
|
1036
1038
|
...
|
|
1037
1039
|
|
|
1040
|
+
def get_active_targets(self) -> dict[Instrument, TargetPosition]:
|
|
1041
|
+
"""
|
|
1042
|
+
Get active target positions for each instrument in the universe.
|
|
1043
|
+
Target position (TP) is considered active if
|
|
1044
|
+
1. signal (S) is sent, converted to a TP, and position is open
|
|
1045
|
+
2. S is sent, converted to a TP, and limit order is sent for opening
|
|
1046
|
+
|
|
1047
|
+
So when position is closed TP (because of opposite signal or stop loss/take profit) becomes inactive.
|
|
1048
|
+
|
|
1049
|
+
Returns:
|
|
1050
|
+
dict[Instrument, TargetPosition]: Dictionary mapping instruments to their active targets.
|
|
1051
|
+
"""
|
|
1052
|
+
...
|
|
1053
|
+
|
|
1054
|
+
def emit_signal(self, signal: Signal) -> None:
|
|
1055
|
+
"""
|
|
1056
|
+
Emit a signal for processing
|
|
1057
|
+
"""
|
|
1058
|
+
...
|
|
1059
|
+
|
|
1038
1060
|
|
|
1039
1061
|
class IWarmupStateSaver:
|
|
1040
1062
|
"""
|
|
@@ -1057,6 +1079,14 @@ class IWarmupStateSaver:
|
|
|
1057
1079
|
"""Get warmup orders."""
|
|
1058
1080
|
...
|
|
1059
1081
|
|
|
1082
|
+
def set_warmup_active_targets(self, active_targets: dict[Instrument, TargetPosition]) -> None:
|
|
1083
|
+
"""Set warmup active targets."""
|
|
1084
|
+
...
|
|
1085
|
+
|
|
1086
|
+
def get_warmup_active_targets(self) -> dict[Instrument, TargetPosition]:
|
|
1087
|
+
"""Get warmup active targets."""
|
|
1088
|
+
...
|
|
1089
|
+
|
|
1060
1090
|
|
|
1061
1091
|
@dataclass
|
|
1062
1092
|
class StrategyState:
|
|
@@ -1145,8 +1175,6 @@ class IPositionGathering:
|
|
|
1145
1175
|
res = {}
|
|
1146
1176
|
if targets:
|
|
1147
1177
|
for t in targets:
|
|
1148
|
-
if t.is_service: # we skip processing service positions
|
|
1149
|
-
continue
|
|
1150
1178
|
try:
|
|
1151
1179
|
res[t.instrument] = self.alter_position_size(ctx, t)
|
|
1152
1180
|
except Exception as ex:
|
|
@@ -1237,6 +1265,16 @@ class PositionsTracker:
|
|
|
1237
1265
|
"""
|
|
1238
1266
|
...
|
|
1239
1267
|
|
|
1268
|
+
def restore_position_from_target(self, ctx: IStrategyContext, target: TargetPosition):
|
|
1269
|
+
"""
|
|
1270
|
+
Restore active position and tracking from the target.
|
|
1271
|
+
|
|
1272
|
+
Args:
|
|
1273
|
+
- ctx: Strategy context object.
|
|
1274
|
+
- target: Target position to restore from.
|
|
1275
|
+
"""
|
|
1276
|
+
...
|
|
1277
|
+
|
|
1240
1278
|
|
|
1241
1279
|
@dataclass
|
|
1242
1280
|
class HealthMetrics:
|
|
@@ -1515,6 +1553,7 @@ class StateResolverProtocol(Protocol):
|
|
|
1515
1553
|
ctx: "IStrategyContext",
|
|
1516
1554
|
sim_positions: dict[Instrument, Position],
|
|
1517
1555
|
sim_orders: dict[Instrument, list[Order]],
|
|
1556
|
+
sim_active_targets: dict[Instrument, TargetPosition],
|
|
1518
1557
|
) -> None:
|
|
1519
1558
|
"""
|
|
1520
1559
|
Resolve position mismatches between warmup simulation and live trading.
|
|
@@ -1523,6 +1562,7 @@ class StateResolverProtocol(Protocol):
|
|
|
1523
1562
|
ctx (IStrategyContext): The strategy context
|
|
1524
1563
|
sim_positions (dict[Instrument, Position]): Positions from the simulation
|
|
1525
1564
|
sim_orders (dict[Instrument, list[Order]]): Orders from the simulation
|
|
1565
|
+
sim_active_targets (dict[Instrument, TargetPosition]): Active targets from the simulation
|
|
1526
1566
|
"""
|
|
1527
1567
|
...
|
|
1528
1568
|
|
qubx/core/loggers.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any
|
|
1
|
+
from typing import Any
|
|
2
2
|
|
|
3
3
|
import numpy as np
|
|
4
4
|
|
|
@@ -8,6 +8,7 @@ from qubx.core.basics import (
|
|
|
8
8
|
Deal,
|
|
9
9
|
Instrument,
|
|
10
10
|
Position,
|
|
11
|
+
Signal,
|
|
11
12
|
TargetPosition,
|
|
12
13
|
)
|
|
13
14
|
from qubx.core.series import time_as_nsec
|
|
@@ -32,7 +33,7 @@ class LogsWriter:
|
|
|
32
33
|
self.strategy_id = strategy_id
|
|
33
34
|
self.run_id = run_id
|
|
34
35
|
|
|
35
|
-
def write_data(self, log_type: str, data:
|
|
36
|
+
def write_data(self, log_type: str, data: list[dict[str, Any]]):
|
|
36
37
|
pass
|
|
37
38
|
|
|
38
39
|
def flush_data(self):
|
|
@@ -78,7 +79,7 @@ class PositionsDumper(_BaseIntervalDumper):
|
|
|
78
79
|
so we could check current situation.
|
|
79
80
|
"""
|
|
80
81
|
|
|
81
|
-
positions:
|
|
82
|
+
positions: dict[Instrument, Position]
|
|
82
83
|
_writer: LogsWriter
|
|
83
84
|
|
|
84
85
|
def __init__(
|
|
@@ -155,15 +156,15 @@ class ExecutionsLogger(_BaseIntervalDumper):
|
|
|
155
156
|
"""
|
|
156
157
|
|
|
157
158
|
_writer: LogsWriter
|
|
158
|
-
_deals:
|
|
159
|
+
_deals: list[tuple[Instrument, Deal]]
|
|
159
160
|
|
|
160
161
|
def __init__(self, writer: LogsWriter, max_records=10) -> None:
|
|
161
162
|
super().__init__(None) # no intervals
|
|
162
163
|
self._writer = writer
|
|
163
164
|
self._max_records = max_records
|
|
164
|
-
self._deals
|
|
165
|
+
self._deals = []
|
|
165
166
|
|
|
166
|
-
def record_deals(self, instrument: Instrument, deals:
|
|
167
|
+
def record_deals(self, instrument: Instrument, deals: list[Deal]):
|
|
167
168
|
for d in deals:
|
|
168
169
|
self._deals.append((instrument, d))
|
|
169
170
|
l_time = d.time
|
|
@@ -201,54 +202,77 @@ class ExecutionsLogger(_BaseIntervalDumper):
|
|
|
201
202
|
self._writer.flush_data()
|
|
202
203
|
|
|
203
204
|
|
|
204
|
-
class
|
|
205
|
+
class SignalsAndTargetsLogger(_BaseIntervalDumper):
|
|
205
206
|
"""
|
|
206
|
-
Signals logger - save signals generated by strategy
|
|
207
|
+
Signals and targets logger - save signals generated by strategy
|
|
207
208
|
"""
|
|
208
209
|
|
|
209
210
|
_writer: LogsWriter
|
|
210
|
-
_targets:
|
|
211
|
+
_targets: list[TargetPosition]
|
|
212
|
+
_signals: list[Signal]
|
|
211
213
|
|
|
212
214
|
def __init__(self, writer: LogsWriter, max_records=100) -> None:
|
|
213
215
|
super().__init__(None)
|
|
214
216
|
self._writer = writer
|
|
215
217
|
self._max_records = max_records
|
|
216
218
|
self._targets = []
|
|
219
|
+
self._signals = []
|
|
217
220
|
|
|
218
|
-
def
|
|
221
|
+
def record_targets(self, signals: list[TargetPosition]):
|
|
219
222
|
self._targets.extend(signals)
|
|
220
223
|
|
|
221
224
|
if len(self._targets) >= self._max_records:
|
|
222
225
|
self.dump(None, None)
|
|
223
226
|
|
|
227
|
+
def record_signals(self, signals: list[Signal]):
|
|
228
|
+
self._signals.extend(signals)
|
|
229
|
+
|
|
230
|
+
if len(self._signals) >= self._max_records:
|
|
231
|
+
self.dump(None, None)
|
|
232
|
+
|
|
224
233
|
def dump(self, interval_start_time: np.datetime64 | None, actual_timestamp: np.datetime64 | None):
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
234
|
+
t_data = [
|
|
235
|
+
{
|
|
236
|
+
"timestamp": t.time,
|
|
237
|
+
"symbol": t.instrument.symbol,
|
|
238
|
+
"exchange": t.instrument.exchange,
|
|
239
|
+
"market_type": t.instrument.market_type,
|
|
240
|
+
"target_position": t.target_position_size,
|
|
241
|
+
"entry_price": t.entry_price,
|
|
242
|
+
"take_price": t.take_price,
|
|
243
|
+
"stop_price": t.stop_price,
|
|
244
|
+
}
|
|
245
|
+
for t in self._targets
|
|
246
|
+
]
|
|
247
|
+
|
|
248
|
+
s_data = [
|
|
249
|
+
{
|
|
250
|
+
"timestamp": s.time,
|
|
251
|
+
"symbol": s.instrument.symbol,
|
|
252
|
+
"exchange": s.instrument.exchange,
|
|
253
|
+
"market_type": s.instrument.market_type,
|
|
254
|
+
"signal": s.signal,
|
|
255
|
+
"reference_price": s.reference_price,
|
|
256
|
+
"price": s.price,
|
|
257
|
+
"take": s.take,
|
|
258
|
+
"stop": s.stop,
|
|
259
|
+
"group": s.group,
|
|
260
|
+
"comment": s.comment,
|
|
261
|
+
"service": s.is_service,
|
|
262
|
+
}
|
|
263
|
+
for s in self._signals
|
|
264
|
+
]
|
|
265
|
+
self._writer.write_data("targets", t_data)
|
|
244
266
|
self._targets.clear()
|
|
245
|
-
|
|
267
|
+
|
|
268
|
+
self._writer.write_data("signals", s_data)
|
|
269
|
+
self._signals.clear()
|
|
246
270
|
|
|
247
271
|
def store(self, timestamp: np.datetime64):
|
|
248
272
|
pass
|
|
249
273
|
|
|
250
274
|
def close(self):
|
|
251
|
-
if self._targets:
|
|
275
|
+
if self._targets or self._signals:
|
|
252
276
|
self.dump(None, None)
|
|
253
277
|
self._writer.flush_data()
|
|
254
278
|
|
|
@@ -298,7 +322,7 @@ class StrategyLogging:
|
|
|
298
322
|
portfolio_logger: PortfolioLogger | None = None
|
|
299
323
|
executions_logger: ExecutionsLogger | None = None
|
|
300
324
|
balance_logger: BalanceLogger | None = None
|
|
301
|
-
signals_logger:
|
|
325
|
+
signals_logger: SignalsAndTargetsLogger | None = None
|
|
302
326
|
heartbeat_freq: np.timedelta64 | None = None
|
|
303
327
|
|
|
304
328
|
_last_heartbeat_ts: np.datetime64 | None = None
|
|
@@ -328,7 +352,7 @@ class StrategyLogging:
|
|
|
328
352
|
|
|
329
353
|
# - store signals
|
|
330
354
|
if num_signals_records_to_write >= 1:
|
|
331
|
-
self.signals_logger =
|
|
355
|
+
self.signals_logger = SignalsAndTargetsLogger(logs_writer, num_signals_records_to_write)
|
|
332
356
|
|
|
333
357
|
# - balance logger
|
|
334
358
|
self.balance_logger = BalanceLogger(logs_writer, positions_log_freq)
|
|
@@ -388,13 +412,17 @@ class StrategyLogging:
|
|
|
388
412
|
# - log heartbeat
|
|
389
413
|
self._log_heartbeat(timestamp)
|
|
390
414
|
|
|
391
|
-
def save_deals(self, instrument: Instrument, deals:
|
|
415
|
+
def save_deals(self, instrument: Instrument, deals: list[Deal]):
|
|
392
416
|
if self.executions_logger:
|
|
393
417
|
self.executions_logger.record_deals(instrument, deals)
|
|
394
418
|
|
|
395
|
-
def
|
|
419
|
+
def save_targets(self, targets: list[TargetPosition]):
|
|
396
420
|
if self.signals_logger and targets:
|
|
397
|
-
self.signals_logger.
|
|
421
|
+
self.signals_logger.record_targets(targets)
|
|
422
|
+
|
|
423
|
+
def save_signals(self, signals: list[Signal]):
|
|
424
|
+
if self.signals_logger and signals:
|
|
425
|
+
self.signals_logger.record_signals(signals)
|
|
398
426
|
|
|
399
427
|
def _log_heartbeat(self, timestamp: np.datetime64):
|
|
400
428
|
if not self.heartbeat_freq:
|