Qubx 0.6.59__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.61__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/helpers.py +6 -2
- qubx/core/interfaces.py +48 -3
- qubx/core/loggers.py +64 -36
- qubx/core/metrics.py +20 -9
- qubx/core/mixins/processing.py +190 -92
- qubx/core/mixins/universe.py +12 -5
- qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/core/series.pxd +1 -0
- qubx/core/series.pyi +2 -0
- qubx/core/series.pyx +13 -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 +166 -39
- 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.61.dist-info}/METADATA +1 -1
- {qubx-0.6.59.dist-info → qubx-0.6.61.dist-info}/RECORD +38 -37
- {qubx-0.6.59.dist-info → qubx-0.6.61.dist-info}/LICENSE +0 -0
- {qubx-0.6.59.dist-info → qubx-0.6.61.dist-info}/WHEEL +0 -0
- {qubx-0.6.59.dist-info → qubx-0.6.61.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/helpers.py
CHANGED
|
@@ -276,6 +276,10 @@ def _parse_schedule_spec(schedule: str) -> dict[str, str]:
|
|
|
276
276
|
return {k: v for k, v in m.groupdict().items() if v} if m else {}
|
|
277
277
|
|
|
278
278
|
|
|
279
|
+
def _to_dt_64(time: float) -> np.datetime64:
|
|
280
|
+
return np.datetime64(int(time * 1000000000), "ns")
|
|
281
|
+
|
|
282
|
+
|
|
279
283
|
def process_schedule_spec(spec_str: str | None) -> dict[str, Any]:
|
|
280
284
|
AS_INT = lambda d, k: int(d.get(k, 0)) # noqa: E731
|
|
281
285
|
S = lambda s: [x for x in re.split(r"[, ]", s) if x] # noqa: E731
|
|
@@ -409,11 +413,11 @@ class BasicScheduler:
|
|
|
409
413
|
prev_time = iter.get_prev()
|
|
410
414
|
next_time = iter.get_next(start_time=start_time)
|
|
411
415
|
if next_time:
|
|
412
|
-
self._scdlr.enterabs(next_time, 1, self._trigger, (event, prev_time, next_time))
|
|
416
|
+
self._scdlr.enterabs(next_time, 1, self._trigger, (event, _to_dt_64(prev_time), _to_dt_64(next_time)))
|
|
413
417
|
|
|
414
418
|
# - update next nearest time
|
|
415
419
|
self._next_times[event] = next_time
|
|
416
|
-
self._next_nearest_time =
|
|
420
|
+
self._next_nearest_time = _to_dt_64(min(self._next_times.values()))
|
|
417
421
|
# logger.debug(f" >>> ({event}) task is scheduled at {self._next_nearest_time}")
|
|
418
422
|
|
|
419
423
|
return True
|
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:
|
|
@@ -1118,6 +1148,11 @@ class IStrategyContext(
|
|
|
1118
1148
|
"""Check if the strategy context is running in simulation mode."""
|
|
1119
1149
|
return False
|
|
1120
1150
|
|
|
1151
|
+
@property
|
|
1152
|
+
def is_live_or_warmup(self) -> bool:
|
|
1153
|
+
"""Check if the strategy context is running in live or warmup mode."""
|
|
1154
|
+
return not self.is_simulation or self.is_warmup_in_progress
|
|
1155
|
+
|
|
1121
1156
|
@property
|
|
1122
1157
|
def is_paper_trading(self) -> bool:
|
|
1123
1158
|
"""Check if the strategy context is running in simulated trading mode."""
|
|
@@ -1145,8 +1180,6 @@ class IPositionGathering:
|
|
|
1145
1180
|
res = {}
|
|
1146
1181
|
if targets:
|
|
1147
1182
|
for t in targets:
|
|
1148
|
-
if t.is_service: # we skip processing service positions
|
|
1149
|
-
continue
|
|
1150
1183
|
try:
|
|
1151
1184
|
res[t.instrument] = self.alter_position_size(ctx, t)
|
|
1152
1185
|
except Exception as ex:
|
|
@@ -1237,6 +1270,16 @@ class PositionsTracker:
|
|
|
1237
1270
|
"""
|
|
1238
1271
|
...
|
|
1239
1272
|
|
|
1273
|
+
def restore_position_from_target(self, ctx: IStrategyContext, target: TargetPosition):
|
|
1274
|
+
"""
|
|
1275
|
+
Restore active position and tracking from the target.
|
|
1276
|
+
|
|
1277
|
+
Args:
|
|
1278
|
+
- ctx: Strategy context object.
|
|
1279
|
+
- target: Target position to restore from.
|
|
1280
|
+
"""
|
|
1281
|
+
...
|
|
1282
|
+
|
|
1240
1283
|
|
|
1241
1284
|
@dataclass
|
|
1242
1285
|
class HealthMetrics:
|
|
@@ -1515,6 +1558,7 @@ class StateResolverProtocol(Protocol):
|
|
|
1515
1558
|
ctx: "IStrategyContext",
|
|
1516
1559
|
sim_positions: dict[Instrument, Position],
|
|
1517
1560
|
sim_orders: dict[Instrument, list[Order]],
|
|
1561
|
+
sim_active_targets: dict[Instrument, TargetPosition],
|
|
1518
1562
|
) -> None:
|
|
1519
1563
|
"""
|
|
1520
1564
|
Resolve position mismatches between warmup simulation and live trading.
|
|
@@ -1523,6 +1567,7 @@ class StateResolverProtocol(Protocol):
|
|
|
1523
1567
|
ctx (IStrategyContext): The strategy context
|
|
1524
1568
|
sim_positions (dict[Instrument, Position]): Positions from the simulation
|
|
1525
1569
|
sim_orders (dict[Instrument, list[Order]]): Orders from the simulation
|
|
1570
|
+
sim_active_targets (dict[Instrument, TargetPosition]): Active targets from the simulation
|
|
1526
1571
|
"""
|
|
1527
1572
|
...
|
|
1528
1573
|
|