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.

@@ -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 - exact price when signal was generated
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
- - allow_override: bool - if True, and there is another signal for the same instrument, then override current.
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
- return f"{self.group}{_r} {self.signal:+f} {self.instrument}{_p}{_s}{_t}{_c}"
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 TargetPosition:
180
+ class InitializingSignal(Signal):
112
181
  """
113
- Class for presenting target position calculated from signal
182
+ Special signal type for post-warmup initialization
114
183
  """
115
184
 
116
- time: dt_64 # time when position was set
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
- return f"{'::: INFORMATIVE ::: ' if self.is_service else ''}Target {self.target_position_size:+f} for {self.signal}"
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(self, instrument: Instrument, timeframe: str | None = None, length: int | None = None, consolidated: bool = True) -> pd.DataFrame:
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, Dict, List, Tuple
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: List[Dict[str, Any]]):
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: Dict[Instrument, Position]
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: List[Tuple[Instrument, Deal]]
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: List[Tuple[Instrument, Deal]] = []
165
+ self._deals = []
165
166
 
166
- def record_deals(self, instrument: Instrument, deals: List[Deal]):
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 SignalsLogger(_BaseIntervalDumper):
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: List[TargetPosition]
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 record_signals(self, signals: List[TargetPosition]):
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
- data = []
226
- for s in self._targets:
227
- data.append(
228
- {
229
- "timestamp": s.time,
230
- "symbol": s.instrument.symbol,
231
- "exchange": s.instrument.exchange,
232
- "market_type": s.instrument.market_type,
233
- "signal": s.signal.signal,
234
- "target_position": s.target_position_size,
235
- "reference_price": s.signal.reference_price,
236
- "price": s.price,
237
- "take": s.take,
238
- "stop": s.stop,
239
- "group": s.signal.group,
240
- "comment": s.signal.comment,
241
- "service": s.is_service,
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
- self._writer.write_data("signals", data)
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: SignalsLogger | None = None
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 = SignalsLogger(logs_writer, num_signals_records_to_write)
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: List[Deal]):
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 save_signals_targets(self, targets: List[TargetPosition]):
419
+ def save_targets(self, targets: list[TargetPosition]):
396
420
  if self.signals_logger and targets:
397
- self.signals_logger.record_signals(targets)
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: