Qubx 0.6.76__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.78__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/cli/release.py CHANGED
@@ -84,9 +84,12 @@ def resolve_relative_import(relative_module: str, file_path: str, project_root:
84
84
  # Get the directory containing the file (remove filename)
85
85
  file_dir = os.path.dirname(rel_file_path)
86
86
 
87
- # Convert file directory path to module path
87
+ # Convert file directory path to module path
88
88
  if file_dir:
89
89
  current_module_parts = file_dir.replace(os.sep, ".").split(".")
90
+ # Remove 'src' prefix if present (common Python project structure)
91
+ if current_module_parts[0] == "src" and len(current_module_parts) > 1:
92
+ current_module_parts = current_module_parts[1:]
90
93
  else:
91
94
  current_module_parts = []
92
95
 
@@ -684,8 +687,8 @@ def _copy_dependencies(strategy_path: str, pyproject_root: str, release_dir: str
684
687
  if _src_root is None:
685
688
  raise DependencyResolutionError(f"Could not find the source root for {_src_dir} in {pyproject_root}")
686
689
 
687
- # Now call _get_imports with the correct source root directory
688
- _imports = _get_imports(strategy_path, _src_root, [_src_dir])
690
+ # Now call _get_imports with the correct source root directory and pyproject_root for relative imports
691
+ _imports = _get_imports(strategy_path, _src_root, [_src_dir], pyproject_root)
689
692
 
690
693
  # Validate all dependencies before copying
691
694
  valid_imports, missing_dependencies = _validate_dependencies(_imports, _src_root, _src_dir)
@@ -920,7 +923,7 @@ def _create_zip_archive(output_dir: str, release_dir: str, tag: str) -> None:
920
923
  shutil.rmtree(release_dir)
921
924
 
922
925
 
923
- def _get_imports(file_name: str, current_directory: str, what_to_look: list[str]) -> list[Import]:
926
+ def _get_imports(file_name: str, current_directory: str, what_to_look: list[str], pyproject_root: str | None = None, visited: set[str] | None = None) -> list[Import]:
924
927
  """
925
928
  Recursively get all imports from a file and its dependencies.
926
929
 
@@ -928,6 +931,8 @@ def _get_imports(file_name: str, current_directory: str, what_to_look: list[str]
928
931
  file_name: Path to the Python file to analyze
929
932
  current_directory: Root directory for resolving imports
930
933
  what_to_look: List of module prefixes to filter for
934
+ pyproject_root: Root directory of the project for resolving relative imports
935
+ visited: Set of already visited files to prevent infinite recursion
931
936
 
932
937
  Returns:
933
938
  List of Import objects for all discovered dependencies
@@ -935,8 +940,20 @@ def _get_imports(file_name: str, current_directory: str, what_to_look: list[str]
935
940
  Raises:
936
941
  DependencyResolutionError: If a required dependency cannot be found or processed
937
942
  """
943
+ # Initialize visited set if not provided
944
+ if visited is None:
945
+ visited = set()
946
+
947
+ # Skip if already visited to prevent infinite recursion
948
+ if file_name in visited:
949
+ return []
950
+ visited.add(file_name)
951
+
952
+ # Use pyproject_root if provided, otherwise use current_directory as fallback
953
+ project_root_for_resolution = pyproject_root or current_directory
954
+
938
955
  try:
939
- imports = list(get_imports(file_name, what_to_look, project_root=current_directory))
956
+ imports = list(get_imports(file_name, what_to_look, project_root=project_root_for_resolution))
940
957
  except (SyntaxError, FileNotFoundError) as e:
941
958
  raise DependencyResolutionError(f"Failed to parse imports from {file_name}: {e}")
942
959
 
@@ -959,7 +976,7 @@ def _get_imports(file_name: str, current_directory: str, what_to_look: list[str]
959
976
  if dependency_file:
960
977
  # Recursively process the dependency
961
978
  try:
962
- imports.extend(_get_imports(dependency_file, current_directory, what_to_look))
979
+ imports.extend(_get_imports(dependency_file, current_directory, what_to_look, pyproject_root, visited))
963
980
  except DependencyResolutionError as e:
964
981
  # Log nested dependency errors but continue processing
965
982
  logger.warning(f"Failed to resolve nested dependency: {e}")
@@ -28,6 +28,7 @@ from qubx.utils.marketdata.ccxt import ccxt_symbol_to_instrument
28
28
  from qubx.utils.misc import AsyncThreadLoop
29
29
 
30
30
  from .exceptions import CcxtSymbolNotRecognized
31
+ from .exchange_manager import ExchangeManager
31
32
  from .utils import (
32
33
  ccxt_convert_balance,
33
34
  ccxt_convert_deal_info,
@@ -46,7 +47,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
46
47
  Subscribes to account information from the exchange.
47
48
  """
48
49
 
49
- exchange: cxp.Exchange
50
+ exchange_manager: ExchangeManager
50
51
  channel: CtrlChannel
51
52
  base_currency: str
52
53
  balance_interval: str
@@ -69,7 +70,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
69
70
  def __init__(
70
71
  self,
71
72
  account_id: str,
72
- exchange: cxp.Exchange,
73
+ exchange_manager: ExchangeManager,
73
74
  channel: CtrlChannel,
74
75
  time_provider: ITimeProvider,
75
76
  base_currency: str,
@@ -90,7 +91,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
90
91
  tcc=tcc,
91
92
  initial_capital=0,
92
93
  )
93
- self.exchange = exchange
94
+ self.exchange_manager = exchange_manager
94
95
  self.channel = channel
95
96
  self.max_retries = max_retries
96
97
  self.balance_interval = balance_interval
@@ -99,7 +100,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
99
100
  self.open_order_interval = open_order_interval
100
101
  self.open_order_backoff = open_order_backoff
101
102
  self.max_position_restore_days = max_position_restore_days
102
- self._loop = AsyncThreadLoop(exchange.asyncio_loop)
103
+ self._loop = AsyncThreadLoop(exchange_manager.exchange.asyncio_loop)
103
104
  self._is_running = False
104
105
  self._polling_tasks = {}
105
106
  self._polling_to_init = defaultdict(bool)
@@ -125,7 +126,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
125
126
 
126
127
  self._is_running = True
127
128
 
128
- if not self.exchange.isSandboxModeEnabled:
129
+ if not self.exchange_manager.exchange.isSandboxModeEnabled:
129
130
  # - start polling tasks
130
131
  self._polling_tasks["balance"] = self._loop.submit(
131
132
  self._poller("balance", self._update_balance, self.balance_interval)
@@ -178,8 +179,8 @@ class CcxtAccountProcessor(BasicAccountProcessor):
178
179
 
179
180
  def _get_instrument_for_currency(self, currency: str) -> Instrument:
180
181
  symbol = f"{currency}/{self.base_currency}"
181
- market = self.exchange.market(symbol)
182
- exchange_name = self.exchange.name
182
+ market = self.exchange_manager.exchange.market(symbol)
183
+ exchange_name = self.exchange_manager.exchange.name
183
184
  assert exchange_name is not None
184
185
  return ccxt_symbol_to_instrument(exchange_name, market)
185
186
 
@@ -267,7 +268,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
267
268
 
268
269
  async def _update_balance(self) -> None:
269
270
  """Fetch and update balances from exchange"""
270
- balances_raw = await self.exchange.fetch_balance()
271
+ balances_raw = await self.exchange_manager.exchange.fetch_balance()
271
272
  balances = ccxt_convert_balance(balances_raw)
272
273
  current_balances = self.get_balances()
273
274
 
@@ -292,8 +293,8 @@ class CcxtAccountProcessor(BasicAccountProcessor):
292
293
 
293
294
  async def _update_positions(self) -> None:
294
295
  # fetch and update positions from exchange
295
- ccxt_positions = await self.exchange.fetch_positions()
296
- positions = ccxt_convert_positions(ccxt_positions, self.exchange.name, self.exchange.markets) # type: ignore
296
+ ccxt_positions = await self.exchange_manager.exchange.fetch_positions()
297
+ positions = ccxt_convert_positions(ccxt_positions, self.exchange_manager.exchange.name, self.exchange_manager.exchange.markets) # type: ignore
297
298
  # update required instruments that we need to subscribe to
298
299
  self._required_instruments.update([p.instrument for p in positions])
299
300
  # update positions
@@ -358,7 +359,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
358
359
  if _fetch_instruments:
359
360
  logger.debug(f"Fetching missing tickers for {_fetch_instruments}")
360
361
  _fetch_symbols = [instrument_to_ccxt_symbol(instr) for instr in _fetch_instruments]
361
- tickers: dict[str, dict] = await self.exchange.fetch_tickers(_fetch_symbols)
362
+ tickers: dict[str, dict] = await self.exchange_manager.exchange.fetch_tickers(_fetch_symbols)
362
363
  for symbol, ticker in tickers.items():
363
364
  instr = _symbol_to_instrument.get(symbol)
364
365
  if instr is not None:
@@ -457,7 +458,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
457
458
 
458
459
  async def _cancel_order(order: Order) -> None:
459
460
  try:
460
- await self.exchange.cancel_order(order.id, symbol=instrument_to_ccxt_symbol(order.instrument))
461
+ await self.exchange_manager.exchange.cancel_order(order.id, symbol=instrument_to_ccxt_symbol(order.instrument))
461
462
  logger.debug(
462
463
  f" :: [SYNC] Canceled {order.id} {order.instrument.symbol} {order.side} {order.quantity} @ {order.price} ({order.status})"
463
464
  )
@@ -475,7 +476,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
475
476
  ) -> dict[str, Order]:
476
477
  _start_ms = self._get_start_time_in_ms(days_before) if limit is None else None
477
478
  _ccxt_symbol = instrument_to_ccxt_symbol(instrument)
478
- _fetcher = self.exchange.fetch_open_orders if is_open else self.exchange.fetch_orders
479
+ _fetcher = self.exchange_manager.exchange.fetch_open_orders if is_open else self.exchange_manager.exchange.fetch_orders
479
480
  _raw_orders = await _fetcher(_ccxt_symbol, since=_start_ms, limit=limit)
480
481
  _orders = [ccxt_convert_order_info(instrument, o) for o in _raw_orders]
481
482
  _id_to_order = {o.id: o for o in _orders}
@@ -484,7 +485,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
484
485
  async def _fetch_deals(self, instrument: Instrument, days_before: int = 30) -> list[Deal]:
485
486
  _start_ms = self._get_start_time_in_ms(days_before)
486
487
  _ccxt_symbol = instrument_to_ccxt_symbol(instrument)
487
- deals_data = await self.exchange.fetch_my_trades(_ccxt_symbol, since=_start_ms)
488
+ deals_data = await self.exchange_manager.exchange.fetch_my_trades(_ccxt_symbol, since=_start_ms)
488
489
  deals: list[Deal] = [ccxt_convert_deal_info(o) for o in deals_data]
489
490
  return sorted(deals, key=lambda x: x.time) if deals else []
490
491
 
@@ -530,9 +531,9 @@ class CcxtAccountProcessor(BasicAccountProcessor):
530
531
  _symbol_to_instrument = {}
531
532
 
532
533
  async def _watch_executions():
533
- exec = await self.exchange.watch_orders()
534
+ exec = await self.exchange_manager.exchange.watch_orders()
534
535
  for report in exec:
535
- instrument = ccxt_find_instrument(report["symbol"], self.exchange, _symbol_to_instrument)
536
+ instrument = ccxt_find_instrument(report["symbol"], self.exchange_manager.exchange, _symbol_to_instrument)
536
537
  order = ccxt_convert_order_info(instrument, report)
537
538
  deals = ccxt_extract_deals_from_exec(report)
538
539
  channel.send((instrument, "order", order, False))
@@ -541,7 +542,7 @@ class CcxtAccountProcessor(BasicAccountProcessor):
541
542
 
542
543
  await self._listen_to_stream(
543
544
  subscriber=_watch_executions,
544
- exchange=self.exchange,
545
+ exchange=self.exchange_manager.exchange,
545
546
  channel=channel,
546
547
  name=name,
547
548
  )
@@ -8,7 +8,7 @@ separating subscription concerns from connection management and data handling.
8
8
  from collections import defaultdict
9
9
  from typing import Dict, List, Set
10
10
 
11
- from qubx.core.basics import Instrument
11
+ from qubx.core.basics import DataType, Instrument
12
12
 
13
13
 
14
14
  class SubscriptionManager:
@@ -37,7 +37,7 @@ class SubscriptionManager:
37
37
 
38
38
  # Symbol to instrument mapping for quick lookups
39
39
  self._symbol_to_instrument: dict[str, Instrument] = {}
40
-
40
+
41
41
  # Individual stream mappings: {subscription_type: {instrument: stream_name}}
42
42
  self._individual_streams: dict[str, dict[Instrument, str]] = defaultdict(dict)
43
43
 
@@ -125,7 +125,7 @@ class SubscriptionManager:
125
125
 
126
126
  # Clean up name mapping
127
127
  self._sub_to_name.pop(subscription_type, None)
128
-
128
+
129
129
  # Clean up individual stream mappings
130
130
  self._individual_streams.pop(subscription_type, None)
131
131
 
@@ -163,15 +163,21 @@ class SubscriptionManager:
163
163
  """
164
164
  if instrument is not None:
165
165
  # Return subscriptions (both active and pending) that contain this instrument
166
- active = [sub for sub, instrs in self._subscriptions.items()
167
- if instrument in instrs and self._sub_connection_ready.get(sub, False)]
166
+ active = [
167
+ sub
168
+ for sub, instrs in self._subscriptions.items()
169
+ if instrument in instrs and self._sub_connection_ready.get(sub, False)
170
+ ]
168
171
  pending = [sub for sub, instrs in self._pending_subscriptions.items() if instrument in instrs]
169
172
  return list(set(active + pending))
170
173
 
171
174
  # Return all subscription types that have any instruments (both active and pending)
172
175
  # Only include active subscriptions if connection is ready
173
- active = [sub for sub, instruments in self._subscriptions.items()
174
- if instruments and self._sub_connection_ready.get(sub, False)]
176
+ active = [
177
+ sub
178
+ for sub, instruments in self._subscriptions.items()
179
+ if instruments and self._sub_connection_ready.get(sub, False)
180
+ ]
175
181
  pending = [sub for sub, instruments in self._pending_subscriptions.items() if instruments]
176
182
  return list(set(active + pending))
177
183
 
@@ -211,17 +217,24 @@ class SubscriptionManager:
211
217
 
212
218
  Args:
213
219
  instrument: Instrument to check
214
- subscription_type: Full subscription type (e.g., "ohlc(1m)")
220
+ subscription_type: Base or full subscription type (e.g., "orderbook" or "orderbook(0.0, 20)")
215
221
 
216
222
  Returns:
217
223
  True if subscription is active (not just pending)
218
224
  """
219
- # Only return True if subscription is actually active (not just pending)
220
- return (
221
- subscription_type in self._subscriptions
222
- and instrument in self._subscriptions[subscription_type]
223
- and self._sub_connection_ready.get(subscription_type, False)
224
- )
225
+ # Get the base type for comparison
226
+ base_type = DataType.from_str(subscription_type)[0]
227
+
228
+ # Check if any subscription with matching base type contains the instrument and is ready
229
+ for stored_sub_type, instruments in self._subscriptions.items():
230
+ if (
231
+ DataType.from_str(stored_sub_type)[0] == base_type
232
+ and instrument in instruments
233
+ and self._sub_connection_ready.get(stored_sub_type, False)
234
+ ):
235
+ return True
236
+
237
+ return False
225
238
 
226
239
  def has_pending_subscription(self, instrument: Instrument, subscription_type: str) -> bool:
227
240
  """
@@ -229,16 +242,24 @@ class SubscriptionManager:
229
242
 
230
243
  Args:
231
244
  instrument: Instrument to check
232
- subscription_type: Full subscription type (e.g., "ohlc(1m)")
245
+ subscription_type: Base or full subscription type (e.g., "orderbook" or "orderbook(0.0, 20)")
233
246
 
234
247
  Returns:
235
248
  True if subscription is pending (connection being established)
236
249
  """
237
- return (
238
- subscription_type in self._pending_subscriptions
239
- and instrument in self._pending_subscriptions[subscription_type]
240
- and not self._sub_connection_ready.get(subscription_type, False)
241
- )
250
+ # Get the base type for comparison
251
+ base_type = DataType.from_str(subscription_type)[0]
252
+
253
+ # Check if any pending subscription with matching base type contains the instrument and is not ready
254
+ for stored_sub_type, instruments in self._pending_subscriptions.items():
255
+ if (
256
+ DataType.from_str(stored_sub_type)[0] == base_type
257
+ and instrument in instruments
258
+ and not self._sub_connection_ready.get(stored_sub_type, False)
259
+ ):
260
+ return True
261
+
262
+ return False
242
263
 
243
264
  def get_all_subscribed_instruments(self) -> Set[Instrument]:
244
265
  """
@@ -262,14 +283,14 @@ class SubscriptionManager:
262
283
  True if connection is established and ready
263
284
  """
264
285
  return self._sub_connection_ready.get(subscription_type, False)
265
-
286
+
266
287
  def has_subscription_type(self, subscription_type: str) -> bool:
267
288
  """
268
289
  Check if a subscription type exists (has any instruments).
269
-
290
+
270
291
  Args:
271
292
  subscription_type: Full subscription type (e.g., "ohlc(1m)")
272
-
293
+
273
294
  Returns:
274
295
  True if subscription type has any instruments
275
296
  """
@@ -283,33 +304,33 @@ class SubscriptionManager:
283
304
  Dictionary mapping symbols to instruments
284
305
  """
285
306
  return self._symbol_to_instrument.copy()
286
-
307
+
287
308
  def set_individual_streams(self, subscription_type: str, streams: dict[Instrument, str]) -> None:
288
309
  """
289
310
  Store individual stream mappings for a subscription type.
290
-
311
+
291
312
  Args:
292
313
  subscription_type: Full subscription type (e.g., "ohlc(1m)")
293
314
  streams: Dictionary mapping instrument to stream name
294
315
  """
295
316
  self._individual_streams[subscription_type] = streams
296
-
317
+
297
318
  def get_individual_streams(self, subscription_type: str) -> dict[Instrument, str]:
298
319
  """
299
320
  Get individual stream mappings for a subscription type.
300
-
321
+
301
322
  Args:
302
323
  subscription_type: Full subscription type (e.g., "ohlc(1m)")
303
-
324
+
304
325
  Returns:
305
326
  Dictionary mapping instrument to stream name
306
327
  """
307
328
  return self._individual_streams.get(subscription_type, {})
308
-
329
+
309
330
  def clear_individual_streams(self, subscription_type: str) -> None:
310
331
  """
311
332
  Clear individual stream mappings for a subscription type.
312
-
333
+
313
334
  Args:
314
335
  subscription_type: Full subscription type (e.g., "ohlc(1m)")
315
336
  """
qubx/core/account.py CHANGED
@@ -18,6 +18,7 @@ from qubx.core.basics import (
18
18
  )
19
19
  from qubx.core.helpers import extract_price
20
20
  from qubx.core.interfaces import IAccountProcessor, ISubscriptionManager
21
+ from qubx.core.mixins.utils import EXCHANGE_MAPPINGS
21
22
 
22
23
 
23
24
  class BasicAccountProcessor(IAccountProcessor):
@@ -75,6 +76,9 @@ class BasicAccountProcessor(IAccountProcessor):
75
76
  def get_positions(self, exchange: str | None = None) -> dict[Instrument, Position]:
76
77
  return self._positions
77
78
 
79
+ def get_fees_calculator(self, exchange: str | None = None) -> TransactionCostsCalculator:
80
+ return self._tcc
81
+
78
82
  def get_position(self, instrument: Instrument) -> Position:
79
83
  _pos = self._positions.get(instrument)
80
84
  if _pos is None:
@@ -353,11 +357,20 @@ class CompositeAccountProcessor(IAccountProcessor):
353
357
  """
354
358
  if exchange:
355
359
  if exchange not in self._account_processors:
360
+ # Check if there's a mapping for this exchange
361
+ if exchange in EXCHANGE_MAPPINGS and EXCHANGE_MAPPINGS[exchange] in self._account_processors:
362
+ return EXCHANGE_MAPPINGS[exchange]
356
363
  raise ValueError(f"Unknown exchange: {exchange}")
357
364
  return exchange
358
365
 
359
366
  if instrument:
360
367
  if instrument.exchange not in self._account_processors:
368
+ # Check if there's a mapping for this exchange
369
+ if (
370
+ instrument.exchange in EXCHANGE_MAPPINGS
371
+ and EXCHANGE_MAPPINGS[instrument.exchange] in self._account_processors
372
+ ):
373
+ return EXCHANGE_MAPPINGS[instrument.exchange]
361
374
  raise ValueError(f"Unknown exchange: {instrument.exchange}")
362
375
  return instrument.exchange
363
376
 
@@ -443,6 +456,10 @@ class CompositeAccountProcessor(IAccountProcessor):
443
456
  exch = self._get_exchange(exchange)
444
457
  return self._account_processors[exch].position_report()
445
458
 
459
+ def get_fees_calculator(self, exchange: str | None = None) -> TransactionCostsCalculator:
460
+ exch = self._get_exchange(exchange)
461
+ return self._account_processors[exch].get_fees_calculator()
462
+
446
463
  ########################################################
447
464
  # Leverage information
448
465
  ########################################################
qubx/core/context.py CHANGED
@@ -15,6 +15,7 @@ from qubx.core.basics import (
15
15
  Order,
16
16
  OrderRequest,
17
17
  Position,
18
+ RestoredState,
18
19
  Signal,
19
20
  TargetPosition,
20
21
  Timestamped,
@@ -112,6 +113,7 @@ class StrategyContext(IStrategyContext):
112
113
  strategy_name: str | None = None,
113
114
  strategy_state: StrategyState | None = None,
114
115
  health_monitor: IHealthMonitor | None = None,
116
+ restored_state: RestoredState | None = None,
115
117
  ) -> None:
116
118
  self.account = account
117
119
  self.strategy = self.__instantiate_strategy(strategy, config)
@@ -138,6 +140,7 @@ class StrategyContext(IStrategyContext):
138
140
  self._lifecycle_notifier = lifecycle_notifier
139
141
  self._strategy_state = strategy_state if strategy_state is not None else StrategyState()
140
142
  self._strategy_name = strategy_name if strategy_name is not None else strategy.__class__.__name__
143
+ self._restored_state = restored_state
141
144
 
142
145
  self._health_monitor = health_monitor or DummyHealthMonitor()
143
146
  self.health = self._health_monitor
@@ -150,6 +153,8 @@ class StrategyContext(IStrategyContext):
150
153
  if __position_gathering is None:
151
154
  __position_gathering = position_gathering if position_gathering is not None else SimplePositionGatherer()
152
155
 
156
+ __warmup_position_gathering = SimplePositionGatherer()
157
+
153
158
  self._subscription_manager = SubscriptionManager(
154
159
  data_providers=self._data_providers,
155
160
  default_base_subscription=DataType.ORDERBOOK
@@ -175,9 +180,10 @@ class StrategyContext(IStrategyContext):
175
180
  time_provider=self,
176
181
  account=self.account,
177
182
  position_gathering=__position_gathering,
183
+ warmup_position_gathering=__warmup_position_gathering,
178
184
  )
179
185
  self._trading_manager = TradingManager(
180
- time_provider=self,
186
+ context=self,
181
187
  brokers=self._brokers,
182
188
  account=self.account,
183
189
  strategy_name=self._strategy_name,
@@ -192,6 +198,7 @@ class StrategyContext(IStrategyContext):
192
198
  account=self.account,
193
199
  position_tracker=__position_tracker,
194
200
  position_gathering=__position_gathering,
201
+ warmup_position_gathering=__warmup_position_gathering,
195
202
  universe_manager=self._universe_manager,
196
203
  cache=self._cache,
197
204
  scheduler=self._scheduler,
@@ -455,11 +462,11 @@ class StrategyContext(IStrategyContext):
455
462
  ) -> Order:
456
463
  return self._trading_manager.set_target_position(instrument, target, price, **options)
457
464
 
458
- def close_position(self, instrument: Instrument) -> None:
459
- return self._trading_manager.close_position(instrument)
465
+ def close_position(self, instrument: Instrument, without_signals: bool = False) -> None:
466
+ return self._trading_manager.close_position(instrument, without_signals)
460
467
 
461
- def close_positions(self, market_type: MarketType | None = None) -> None:
462
- return self._trading_manager.close_positions(market_type)
468
+ def close_positions(self, market_type: MarketType | None = None, without_signals: bool = False) -> None:
469
+ return self._trading_manager.close_positions(market_type, without_signals)
463
470
 
464
471
  def cancel_order(self, order_id: str, exchange: str | None = None) -> None:
465
472
  return self._trading_manager.cancel_order(order_id, exchange)
@@ -585,6 +592,9 @@ class StrategyContext(IStrategyContext):
585
592
  def get_warmup_orders(self) -> dict[Instrument, list[Order]]:
586
593
  return self._warmup_orders if self._warmup_orders is not None else {}
587
594
 
595
+ def get_restored_state(self) -> RestoredState | None:
596
+ return self._restored_state
597
+
588
598
  # private methods
589
599
  def __process_incoming_data_loop(self, channel: CtrlChannel):
590
600
  logger.info("[StrategyContext] :: Start processing market data")
qubx/core/interfaces.py CHANGED
@@ -36,6 +36,7 @@ from qubx.core.basics import (
36
36
  Signal,
37
37
  TargetPosition,
38
38
  Timestamped,
39
+ TransactionCostsCalculator,
39
40
  TriggerEvent,
40
41
  dt_64,
41
42
  td_64,
@@ -153,6 +154,17 @@ class IAccountViewer:
153
154
  """
154
155
  ...
155
156
 
157
+ def get_fees_calculator(self, exchange: str | None = None) -> TransactionCostsCalculator:
158
+ """Get the fees calculator.
159
+
160
+ Args:
161
+ exchange: The exchange to get the fees calculator for
162
+
163
+ Returns:
164
+ TransactionCostsCalculator: The transaction costs calculator
165
+ """
166
+ ...
167
+
156
168
  @property
157
169
  def positions(self) -> dict[Instrument, Position]:
158
170
  """[Deprecated: Use get_positions()] Get all current positions.
@@ -673,15 +685,16 @@ class ITradingManager:
673
685
  """
674
686
  ...
675
687
 
676
- def close_position(self, instrument: Instrument) -> None:
688
+ def close_position(self, instrument: Instrument, without_signals: bool = False) -> None:
677
689
  """Close position for an instrument.
678
690
 
679
691
  Args:
680
692
  instrument: The instrument to close position for
693
+ without_signals: If True, trade submitted instead of emitting signal
681
694
  """
682
695
  ...
683
696
 
684
- def close_positions(self, market_type: MarketType | None = None, exchange: str | None = None) -> None:
697
+ def close_positions(self, market_type: MarketType | None = None, without_signals: bool = False) -> None:
685
698
  """Close all positions."""
686
699
  ...
687
700
 
@@ -1200,6 +1213,10 @@ class IStrategyContext(
1200
1213
  """Get the list of exchanges."""
1201
1214
  return []
1202
1215
 
1216
+ def get_restored_state(self) -> "RestoredState | None":
1217
+ """Get the restored state."""
1218
+ return None
1219
+
1203
1220
 
1204
1221
  class IPositionGathering:
1205
1222
  """
@@ -1226,6 +1243,28 @@ class IPositionGathering:
1226
1243
 
1227
1244
  def on_execution_report(self, ctx: IStrategyContext, instrument: Instrument, deal: Deal): ...
1228
1245
 
1246
+ def update(self, ctx: IStrategyContext, instrument: Instrument, update: Timestamped) -> None:
1247
+ """
1248
+ Position gatherer is being updated by new market data.
1249
+
1250
+ Args:
1251
+ ctx: Strategy context object
1252
+ instrument: The instrument for which market data was updated
1253
+ update: The market data update (Quote, Trade, Bar, etc.)
1254
+ """
1255
+ pass
1256
+
1257
+ def restore_from_target_positions(self, ctx: IStrategyContext, target_positions: list[TargetPosition]) -> None:
1258
+ """
1259
+ Restore gatherer state from target positions.
1260
+
1261
+ Args:
1262
+ ctx: Strategy context object
1263
+ target_positions: List of target positions to restore gatherer state from
1264
+ """
1265
+ # Default implementation - subclasses can override if needed
1266
+ pass
1267
+
1229
1268
 
1230
1269
  class IPositionSizer:
1231
1270
  """Interface for calculating target positions from signals."""
@@ -1307,15 +1346,16 @@ class PositionsTracker:
1307
1346
  """
1308
1347
  ...
1309
1348
 
1310
- def restore_position_from_target(self, ctx: IStrategyContext, target: TargetPosition):
1349
+ def restore_position_from_signals(self, ctx: IStrategyContext, signals: list[Signal]) -> None:
1311
1350
  """
1312
- Restore active position and tracking from the target.
1351
+ Restore tracker state from signals.
1313
1352
 
1314
1353
  Args:
1315
- - ctx: Strategy context object.
1316
- - target: Target position to restore from.
1354
+ ctx: Strategy context object
1355
+ signals: List of signals to restore tracker state from
1317
1356
  """
1318
- ...
1357
+ # Default implementation - subclasses can override
1358
+ pass
1319
1359
 
1320
1360
 
1321
1361
  @dataclass
@@ -1350,12 +1390,12 @@ class HealthMetrics:
1350
1390
  @runtime_checkable
1351
1391
  class IDataArrivalListener(Protocol):
1352
1392
  """Interface for components that want to be notified of data arrivals."""
1353
-
1393
+
1354
1394
  def on_data_arrival(self, event_type: str, event_time: dt_64) -> None:
1355
1395
  """Called when new data arrives.
1356
-
1396
+
1357
1397
  Args:
1358
- event_type: Type of data event (e.g., "ohlcv:BTC/USDT:1m")
1398
+ event_type: Type of data event (e.g., "ohlcv:BTC/USDT:1m")
1359
1399
  event_time: Timestamp of the data event
1360
1400
  """
1361
1401
  ...
qubx/core/loggers.py CHANGED
@@ -244,6 +244,7 @@ class SignalsAndTargetsLogger(_BaseIntervalDumper):
244
244
  "entry_price": t.entry_price,
245
245
  "take_price": t.take_price,
246
246
  "stop_price": t.stop_price,
247
+ "options": t.options,
247
248
  }
248
249
  for t in self._targets
249
250
  ]
@@ -262,6 +263,7 @@ class SignalsAndTargetsLogger(_BaseIntervalDumper):
262
263
  "group": s.group,
263
264
  "comment": s.comment,
264
265
  "service": s.is_service,
266
+ "options": s.options,
265
267
  }
266
268
  for s in self._signals
267
269
  ]
@@ -16,6 +16,7 @@ from qubx.core.basics import (
16
16
  Instrument,
17
17
  MarketEvent,
18
18
  Order,
19
+ RestoredState,
19
20
  Signal,
20
21
  TargetPosition,
21
22
  Timestamped,
@@ -59,6 +60,7 @@ class ProcessingManager(IProcessingManager):
59
60
  _account: IAccountProcessor
60
61
  _position_tracker: PositionsTracker
61
62
  _position_gathering: IPositionGathering
63
+ _warmup_position_gathering: IPositionGathering
62
64
  _cache: CachedMarketDataHolder
63
65
  _scheduler: BasicScheduler
64
66
  _universe_manager: IUniverseManager
@@ -101,6 +103,7 @@ class ProcessingManager(IProcessingManager):
101
103
  account: IAccountProcessor,
102
104
  position_tracker: PositionsTracker,
103
105
  position_gathering: IPositionGathering,
106
+ warmup_position_gathering: IPositionGathering,
104
107
  universe_manager: IUniverseManager,
105
108
  cache: CachedMarketDataHolder,
106
109
  scheduler: BasicScheduler,
@@ -152,6 +155,8 @@ class ProcessingManager(IProcessingManager):
152
155
  self._active_targets = {}
153
156
  self._custom_scheduled_methods = {}
154
157
 
158
+ self._warmup_position_gathering = warmup_position_gathering
159
+
155
160
  # - schedule daily delisting check at 23:30 (end of day)
156
161
  self._scheduler.schedule_event("30 23 * * *", "delisting_check")
157
162
 
@@ -254,6 +259,12 @@ class ProcessingManager(IProcessingManager):
254
259
  ):
255
260
  if self._context.get_warmup_positions() or self._context.get_warmup_orders():
256
261
  self._handle_state_resolution()
262
+
263
+ # Restore tracker and gatherer state if available
264
+ restored_state = self._context.get_restored_state()
265
+ if restored_state is not None:
266
+ self._restore_tracker_and_gatherer_state(restored_state)
267
+
257
268
  self._handle_warmup_finished()
258
269
 
259
270
  # - check if it still didn't call on_fit() for first time
@@ -350,6 +361,13 @@ class ProcessingManager(IProcessingManager):
350
361
  else self._position_tracker
351
362
  )
352
363
 
364
+ def _get_position_gatherer(self) -> IPositionGathering:
365
+ return (
366
+ self._position_gathering
367
+ if self._context._strategy_state.is_on_warmup_finished_called
368
+ else self._warmup_position_gathering
369
+ )
370
+
353
371
  def __preprocess_signals_and_split_by_stage(
354
372
  self, signals: list[Signal]
355
373
  ) -> tuple[list[Signal], list[Signal], set[Instrument]]:
@@ -428,7 +446,7 @@ class ProcessingManager(IProcessingManager):
428
446
 
429
447
  # - notify position gatherer for the new target positions
430
448
  if _targets_from_trackers:
431
- self._position_gathering.alter_positions(
449
+ self._get_position_gatherer().alter_positions(
432
450
  self._context, self.__preprocess_and_log_target_positions(_targets_from_trackers)
433
451
  )
434
452
 
@@ -643,15 +661,16 @@ class ProcessingManager(IProcessingManager):
643
661
  # - update tracker
644
662
  _targets_from_tracker = self._get_tracker_for(instrument).update(self._context, instrument, _update)
645
663
 
646
- # TODO: add gatherer update
647
-
648
664
  # - notify position gatherer for the new target positions
649
665
  if _targets_from_tracker:
650
666
  # - tracker generated new targets on update, notify position gatherer
651
- self._position_gathering.alter_positions(
667
+ self._get_position_gatherer().alter_positions(
652
668
  self._context, self.__preprocess_and_log_target_positions(self._as_list(_targets_from_tracker))
653
669
  )
654
670
 
671
+ # - update position gatherer with market data
672
+ self._get_position_gatherer().update(self._context, instrument, _update)
673
+
655
674
  # - check for stale data periodically (only for base data updates)
656
675
  # This ensures we only check when we have new meaningful data
657
676
  if self._stale_data_detection_enabled and self._context._strategy_state.is_on_start_called:
@@ -753,6 +772,37 @@ class ProcessingManager(IProcessingManager):
753
772
 
754
773
  resolver(_ctx, _ctx.get_warmup_positions(), _ctx.get_warmup_orders(), _ctx.get_warmup_active_targets())
755
774
 
775
+ def _restore_tracker_and_gatherer_state(self, restored_state: RestoredState) -> None:
776
+ """
777
+ Restore state for position tracker and gatherer.
778
+
779
+ Args:
780
+ restored_state: The restored state containing signals and target positions
781
+ """
782
+ if not self._is_data_ready():
783
+ return
784
+
785
+ # Restore tracker state from signals
786
+ all_signals = []
787
+ for instrument, signals in restored_state.instrument_to_signal_positions.items():
788
+ all_signals.extend(signals)
789
+
790
+ if all_signals:
791
+ logger.info(f"<yellow>Restoring tracker state from {len(all_signals)} signals</yellow>")
792
+ self._position_tracker.restore_position_from_signals(self._context, all_signals)
793
+
794
+ # Restore gatherer state from latest target positions only
795
+ latest_targets = []
796
+ for instrument, targets in restored_state.instrument_to_target_positions.items():
797
+ if targets: # Only if there are targets for this instrument
798
+ # Get the latest target position (assuming they are sorted by time)
799
+ latest_target = max(targets, key=lambda t: t.time)
800
+ latest_targets.append(latest_target)
801
+
802
+ if latest_targets:
803
+ logger.info(f"<yellow>Restoring gatherer state from {len(latest_targets)} latest target positions</yellow>")
804
+ self._position_gathering.restore_from_target_positions(self._context, latest_targets)
805
+
756
806
  def _handle_warmup_finished(self) -> None:
757
807
  if not self._is_data_ready():
758
808
  return
@@ -856,7 +906,7 @@ class ProcessingManager(IProcessingManager):
856
906
  # - Process all deals first
857
907
  for d in deals:
858
908
  # - notify position gatherer and tracker
859
- self._position_gathering.on_execution_report(self._context, instrument, d)
909
+ self._get_position_gatherer().on_execution_report(self._context, instrument, d)
860
910
  self._get_tracker_for(instrument).on_execution_report(self._context, instrument, d)
861
911
 
862
912
  logger.debug(
@@ -3,7 +3,7 @@ from typing import Any
3
3
  from qubx import logger
4
4
  from qubx.core.basics import Instrument, MarketType, Order, OrderRequest, OrderSide
5
5
  from qubx.core.exceptions import OrderNotFound
6
- from qubx.core.interfaces import IAccountProcessor, IBroker, ITimeProvider, ITradingManager
6
+ from qubx.core.interfaces import IAccountProcessor, IBroker, IStrategyContext, ITimeProvider, ITradingManager
7
7
 
8
8
  from .utils import EXCHANGE_MAPPINGS
9
9
 
@@ -60,7 +60,7 @@ class ClientIdStore:
60
60
 
61
61
 
62
62
  class TradingManager(ITradingManager):
63
- _time_provider: ITimeProvider
63
+ _context: IStrategyContext
64
64
  _brokers: list[IBroker]
65
65
  _account: IAccountProcessor
66
66
  _strategy_name: str
@@ -69,9 +69,9 @@ class TradingManager(ITradingManager):
69
69
  _exchange_to_broker: dict[str, IBroker]
70
70
 
71
71
  def __init__(
72
- self, time_provider: ITimeProvider, brokers: list[IBroker], account: IAccountProcessor, strategy_name: str
72
+ self, context: IStrategyContext, brokers: list[IBroker], account: IAccountProcessor, strategy_name: str
73
73
  ) -> None:
74
- self._time_provider = time_provider
74
+ self._context = context
75
75
  self._brokers = brokers
76
76
  self._account = account
77
77
  self._strategy_name = strategy_name
@@ -156,21 +156,27 @@ class TradingManager(ITradingManager):
156
156
  ) -> Order:
157
157
  raise NotImplementedError("Not implemented yet")
158
158
 
159
- def close_position(self, instrument: Instrument) -> None:
159
+ def close_position(self, instrument: Instrument, without_signals: bool = False) -> None:
160
160
  position = self._account.get_position(instrument)
161
161
 
162
162
  if not position.is_open():
163
163
  logger.debug(f"[<g>{instrument.symbol}</g>] :: Position already closed or zero size")
164
164
  return
165
165
 
166
- closing_amount = -position.quantity
167
- logger.debug(
168
- f"[<g>{instrument.symbol}</g>] :: Closing position {position.quantity} with market order for {closing_amount}"
169
- )
170
-
171
- self.trade(instrument, closing_amount, reduceOnly=True)
166
+ if without_signals:
167
+ closing_amount = -position.quantity
168
+ logger.debug(
169
+ f"[<g>{instrument.symbol}</g>] :: Closing position {position.quantity} with market order for {closing_amount}"
170
+ )
171
+ self.trade(instrument, closing_amount, reduceOnly=True)
172
+ else:
173
+ logger.debug(
174
+ f"[<g>{instrument.symbol}</g>] :: Closing position {position.quantity} by emitting signal with 0 target"
175
+ )
176
+ signal = instrument.signal(self._context, 0, comment="Close position trade")
177
+ self._context.emit_signal(signal)
172
178
 
173
- def close_positions(self, market_type: MarketType | None = None) -> None:
179
+ def close_positions(self, market_type: MarketType | None = None, without_signals: bool = False) -> None:
174
180
  positions = self._account.get_positions()
175
181
 
176
182
  positions_to_close = []
@@ -188,7 +194,7 @@ class TradingManager(ITradingManager):
188
194
  )
189
195
 
190
196
  for instrument in positions_to_close:
191
- self.close_position(instrument)
197
+ self.close_position(instrument, without_signals)
192
198
 
193
199
  def cancel_order(self, order_id: str, exchange: str | None = None) -> None:
194
200
  if not order_id:
@@ -208,7 +214,7 @@ class TradingManager(ITradingManager):
208
214
  self.cancel_order(o.id, instrument.exchange)
209
215
 
210
216
  def _generate_order_client_id(self, symbol: str) -> str:
211
- return self._client_id_store.generate_id(self._time_provider, symbol)
217
+ return self._client_id_store.generate_id(self._context, symbol)
212
218
 
213
219
  def exchanges(self) -> list[str]:
214
220
  return list(self._exchange_to_broker.keys())
@@ -1,4 +1,4 @@
1
- from qubx.core.basics import DataType, Instrument, TargetPosition
1
+ from qubx.core.basics import DataType, Instrument
2
2
  from qubx.core.helpers import CachedMarketDataHolder
3
3
  from qubx.core.interfaces import (
4
4
  IAccountProcessor,
@@ -24,6 +24,7 @@ class UniverseManager(IUniverseManager):
24
24
  _time_provider: ITimeProvider
25
25
  _account: IAccountProcessor
26
26
  _position_gathering: IPositionGathering
27
+ _warmup_position_gathering: IPositionGathering
27
28
  _removal_queue: dict[Instrument, tuple[RemovalPolicy, bool]]
28
29
 
29
30
  def __init__(
@@ -37,6 +38,7 @@ class UniverseManager(IUniverseManager):
37
38
  time_provider: ITimeProvider,
38
39
  account: IAccountProcessor,
39
40
  position_gathering: IPositionGathering,
41
+ warmup_position_gathering: IPositionGathering,
40
42
  ):
41
43
  self._context = context
42
44
  self._strategy = strategy
@@ -50,6 +52,8 @@ class UniverseManager(IUniverseManager):
50
52
  self._instruments = set()
51
53
  self._removal_queue = {}
52
54
 
55
+ self._warmup_position_gathering = warmup_position_gathering
56
+
53
57
  def _has_position(self, instrument: Instrument) -> bool:
54
58
  return (
55
59
  instrument in self._account.positions
@@ -107,6 +111,13 @@ class UniverseManager(IUniverseManager):
107
111
  to_remove.append(instr)
108
112
  return to_remove, to_keep
109
113
 
114
+ def _get_position_gatherer(self) -> IPositionGathering:
115
+ return (
116
+ self._position_gathering
117
+ if self._context._strategy_state.is_on_warmup_finished_called
118
+ else self._warmup_position_gathering
119
+ )
120
+
110
121
  def __cleanup_removal_queue(self, instruments: list[Instrument]):
111
122
  for instr in instruments:
112
123
  # - if it's still in the removal queue, remove it
@@ -181,7 +192,7 @@ class UniverseManager(IUniverseManager):
181
192
  )
182
193
 
183
194
  # - alter positions
184
- self._position_gathering.alter_positions(self._context, exit_targets)
195
+ self._get_position_gatherer().alter_positions(self._context, exit_targets)
185
196
 
186
197
  # - if still open positions close them manually
187
198
  for instr in instruments:
@@ -113,11 +113,17 @@ class IndicatorEmitter(Indicator):
113
113
  emission_tags = self._tags.copy()
114
114
 
115
115
  # Emit the metric with the proper timestamp
116
+ # Convert time to numpy datetime64 if needed
117
+ if isinstance(time, int):
118
+ timestamp = np.datetime64(time, 'ns')
119
+ else:
120
+ timestamp = pd.Timestamp(time).to_datetime64()
121
+
116
122
  self._metric_emitter.emit(
117
123
  name=self._metric_name,
118
124
  value=float(current_value),
119
125
  tags=emission_tags,
120
- timestamp=pd.Timestamp(time),
126
+ timestamp=timestamp,
121
127
  instrument=self._instrument,
122
128
  )
123
129
 
@@ -134,8 +140,7 @@ class IndicatorEmitter(Indicator):
134
140
  f"from '{self._wrapped_indicator.name}': {e}"
135
141
  )
136
142
 
137
- # Return the current value
138
- return current_value
143
+ return float(current_value)
139
144
 
140
145
  @classmethod
141
146
  def wrap_with_emitter(
qubx/emitters/questdb.py CHANGED
@@ -146,7 +146,7 @@ class QuestDBMetricEmitter(BaseMetricEmitter):
146
146
  symbols = {"metric_name": name}
147
147
  symbols.update(tags) # Add all tags as symbols
148
148
 
149
- columns = {"value": round(value, 5)} # Add the value as a column
149
+ columns: dict = {"value": round(value, 5)} # Add the value as a column
150
150
 
151
151
  # Use the provided timestamp if available, otherwise use current time
152
152
  dt_timestamp = self._convert_timestamp(timestamp) if timestamp is not None else datetime.datetime.now()
@@ -56,7 +56,7 @@ class StateResolver:
56
56
  # If signs are opposite, close the live position
57
57
  if live_qty * sim_qty < 0:
58
58
  logger.info(f"Closing position for {instrument.symbol} due to opposite direction: {live_qty} -> 0")
59
- ctx.trade(instrument, -live_qty)
59
+ ctx.emit_signal(InitializingSignal(time=ctx.time(), instrument=instrument, signal=0.0))
60
60
 
61
61
  # If live position is larger than sim position (same direction), reduce it
62
62
  elif abs(live_qty) > abs(sim_qty) and abs(live_qty) > instrument.lot_size:
@@ -64,7 +64,7 @@ class StateResolver:
64
64
  logger.info(
65
65
  f"Reducing position for {instrument.symbol}: {live_qty} -> {sim_qty} (diff: {qty_diff:.4f})"
66
66
  )
67
- ctx.trade(instrument, qty_diff)
67
+ ctx.emit_signal(InitializingSignal(time=ctx.time(), instrument=instrument, signal=sim_qty))
68
68
 
69
69
  # If sim position is larger or equal (same direction), do nothing
70
70
  else:
@@ -73,7 +73,7 @@ class StateResolver:
73
73
  # If the instrument doesn't exist in simulation, close the position
74
74
  else:
75
75
  logger.info(f"Closing position for {instrument.symbol} not in simulation: {live_qty} -> 0")
76
- ctx.trade(instrument, -live_qty)
76
+ ctx.emit_signal(InitializingSignal(time=ctx.time(), instrument=instrument, signal=0.0))
77
77
 
78
78
  @staticmethod
79
79
  def CLOSE_ALL(
@@ -108,7 +108,7 @@ class StateResolver:
108
108
  for instrument, position in live_positions.items():
109
109
  if abs(position.quantity) > instrument.lot_size:
110
110
  logger.info(f"Closing position for {instrument.symbol}: {position.quantity} -> 0")
111
- ctx.trade(instrument, -position.quantity)
111
+ ctx.emit_signal(InitializingSignal(time=ctx.time(), instrument=instrument, signal=0.0))
112
112
 
113
113
  @staticmethod
114
114
  def SYNC_STATE(
qubx/restorers/signal.py CHANGED
@@ -289,7 +289,7 @@ class MongoDBSignalRestorer(ISignalRestorer):
289
289
  continue
290
290
 
291
291
  price = log.get("price") or log.get("reference_price")
292
- options = {key: log[key] for key in ["comment", "size", "meta"] if key in log}
292
+ options = log.get("options", {})
293
293
 
294
294
  signal = Signal(
295
295
  time=recognize_time(log["timestamp"]),
@@ -328,7 +328,7 @@ class MongoDBSignalRestorer(ISignalRestorer):
328
328
 
329
329
  target_size = float(log["target_position"])
330
330
  price = log.get("entry_price", None)
331
- options = {key: log[key] for key in ["comment", "size", "meta"] if key in log}
331
+ options = log.get("options", {})
332
332
 
333
333
  target = TargetPosition(
334
334
  time=recognize_time(log["timestamp"]),
@@ -85,13 +85,14 @@ for x in inspect.getmembers(S, (inspect.ismethod)):
85
85
  # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
86
86
 
87
87
  def _pos_to_dict(p: Position):
88
- mv = round(p.market_value_funds, 3)
88
+ mv = round(p.notional_value, 3)
89
89
  return dict(
90
- Position=round(p.quantity, p.instrument.size_precision),
91
- PnL=p.total_pnl(),
92
- AvgPrice=round(p.position_avg_price_funds, p.instrument.price_precision),
93
- LastPrice=round(p.last_update_price, p.instrument.price_precision),
94
- MktValue=mv)
90
+ MktValue=mv,
91
+ Position=round(p.quantity, p.instrument.size_precision),
92
+ PnL=p.total_pnl(),
93
+ AvgPrice=round(p.position_avg_price_funds, p.instrument.price_precision),
94
+ LastPrice=round(p.last_update_price, p.instrument.price_precision),
95
+ )
95
96
 
96
97
 
97
98
  class ActiveInstrument:
@@ -195,7 +196,6 @@ def portfolio(all=True):
195
196
 
196
197
  d = dict()
197
198
  for s, p in ctx.get_positions().items():
198
- mv = round(p.market_value_funds, 3)
199
199
  if p.quantity != 0.0 or all:
200
200
  d[dequotify(s.symbol, s.quote)] = _pos_to_dict(p)
201
201
 
@@ -205,10 +205,10 @@ def portfolio(all=True):
205
205
  print('-(no open positions yet)-')
206
206
  return
207
207
 
208
- d = d.sort_values('PnL' ,ascending=False)
208
+ d = d.sort_values('MktValue' ,ascending=False)
209
209
  # d = pd.concat((d, pd.Series(dict(TOTAL=d['PnL'].sum()), name='PnL'))).fillna('')
210
210
  d = pd.concat((d, scols(pd.Series(dict(TOTAL=d['PnL'].sum()), name='PnL'), pd.Series(dict(TOTAL=d['MktValue'].sum()), name='MktValue')))).fillna('')
211
- print(tabulate(d, ['Position', 'PnL', 'AvgPrice', 'LastPrice', 'MktValue'], tablefmt='rounded_grid'))
211
+ print(tabulate(d, ['MktValue', 'Position', 'PnL', 'AvgPrice', 'LastPrice'], tablefmt='rounded_grid'))
212
212
 
213
213
  # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
214
214
  __exit = exit
@@ -384,6 +384,7 @@ def create_strategy_context(
384
384
  initializer=_initializer,
385
385
  strategy_name=stg_name,
386
386
  health_monitor=_health_monitor,
387
+ restored_state=restored_state,
387
388
  )
388
389
 
389
390
  return ctx
@@ -509,7 +510,7 @@ def _create_account_processor(
509
510
  return get_ccxt_account(
510
511
  exchange_name,
511
512
  account_id=exchange_name,
512
- exchange=exchange_manager,
513
+ exchange_manager=exchange_manager,
513
514
  channel=channel,
514
515
  time_provider=time_provider,
515
516
  base_currency=creds.base_currency,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: Qubx
3
- Version: 0.6.76
3
+ Version: 0.6.78
4
4
  Summary: Qubx - Quantitative Trading Framework
5
5
  Author: Dmitry Marienko
6
6
  Author-email: dmitry.marienko@xlydian.com
@@ -17,10 +17,10 @@ qubx/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  qubx/cli/commands.py,sha256=z1Qs6jLbrfINk7D6V7n6ruPNLoyq3T-sY66Y-F8LmBE,11714
18
18
  qubx/cli/deploy.py,sha256=pQ9FPOsywDyy8jOjLfrgYTTkKQ-MCixCzbgsG68Q3_0,8319
19
19
  qubx/cli/misc.py,sha256=tP28QxLEzuP8R2xnt8g3JTs9Z7aYy4iVWY4g3VzKTsQ,14777
20
- qubx/cli/release.py,sha256=7xaCcpUSm6aK_SC_F_YIZl-vYToKWnkaZW-Ik8oBcRs,40435
20
+ qubx/cli/release.py,sha256=Kz5aykF9FlAeUgXF59_Mu3vRFxa_qF9dq0ySqLlKKqo,41362
21
21
  qubx/cli/tui.py,sha256=N15UiNEdnWOWYh8E9DNlQCDWdoyP6rMGMhEItogPW88,16491
22
22
  qubx/connectors/ccxt/__init__.py,sha256=HEQ7lM9HS8sED_zfsAHrhFT7F9E7NFGAecwZwNr-TDE,65
23
- qubx/connectors/ccxt/account.py,sha256=HILqsSPfor58NrlP0qYwO5lkNZzUBG-SR5Hy1OSa7_M,24308
23
+ qubx/connectors/ccxt/account.py,sha256=f_Qa3ti5RY6CP3aww04CKvGgc8FYdSOi__FMdscG2y4,24664
24
24
  qubx/connectors/ccxt/adapters/__init__.py,sha256=4qwWer4C9fTIZKUYmcgbMFEwFuYp34UKbb-AoQNvXjc,178
25
25
  qubx/connectors/ccxt/adapters/polling_adapter.py,sha256=UrOAoIfgtoRw7gj6Bmk5lPs_UI8iOxr88DAfp5o5CF8,8962
26
26
  qubx/connectors/ccxt/broker.py,sha256=I6I_BynhwHadKa62nBGmDoj5LhFiEzbUq-1ZDwE25aM,16512
@@ -51,38 +51,38 @@ qubx/connectors/ccxt/handlers/quote.py,sha256=JwQ8mXMpFMdFEpQTx3x_Xaj6VHZanC6_JI
51
51
  qubx/connectors/ccxt/handlers/trade.py,sha256=VspCqw13r9SyvF0N3a31YKIVTzUx5IjFtMDaQeSxblM,4519
52
52
  qubx/connectors/ccxt/reader.py,sha256=uUG1I_ejzTf0f4bCAHpLhBzTUqtNX-JvJGFA4bi7-WU,26602
53
53
  qubx/connectors/ccxt/subscription_config.py,sha256=jbMZ_9US3nvrp6LCVmMXLQnAjXH0xIltzUSPqXJZvgs,3865
54
- qubx/connectors/ccxt/subscription_manager.py,sha256=ZGWf-j4ZTJm-flhVvCs2kLRGqt0zci3p4PSf4nvTKzI,12662
54
+ qubx/connectors/ccxt/subscription_manager.py,sha256=9ZfA6bR6YwtlZqVs6yymKPvYSvy5x3yBuHA_LDbiKgc,13285
55
55
  qubx/connectors/ccxt/subscription_orchestrator.py,sha256=CbZMTRhmgcJZd8cofQbyBDI__N2Lbo1loYfh9_-EkFA,16512
56
56
  qubx/connectors/ccxt/utils.py,sha256=ygUHJdhuuSPCvPULEYK05ZuKExbOnDd-dsJ8mEhDDBA,16378
57
57
  qubx/connectors/ccxt/warmup_service.py,sha256=a7qSFUmgUm6s7qP-ae9RP-j1bR9XyEsNy4SNOLbPk_c,4893
58
58
  qubx/connectors/tardis/data.py,sha256=TlapY1dwc_aQxf4Na9sF620lK9drrg7E9E8gPTGD3FE,31004
59
59
  qubx/connectors/tardis/utils.py,sha256=epThu9DwqbDb7BgScH6fHa_FVpKUaItOqp3JwtKGc5g,9092
60
60
  qubx/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
61
- qubx/core/account.py,sha256=ZuWhV1_d6jgi7Ry0j1dUDllQ6nO3c4TqcsKQ3lWls0E,22951
61
+ qubx/core/account.py,sha256=HhbxVQ-iozVOGr5uL5ZaBGK8TCZi2KoyTaFu79uX3Oo,23880
62
62
  qubx/core/basics.py,sha256=ggK75OsmpH-SCH_Q_2kJmmgFxgrgt4RgO7QipCqUCGk,41301
63
- qubx/core/context.py,sha256=x-lLDJIZLH93Jtm_KmoG0joVwFGe65GeOUeNQt6Is7Q,25573
63
+ qubx/core/context.py,sha256=Yq039nsKmf-IeyvcRdnMs_phVmehoCUqFvQuVnm-d4s,26074
64
64
  qubx/core/deque.py,sha256=3PsmJ5LF76JpsK4Wp5LLogyE15rKn6EDCkNOOWT6EOk,6203
65
65
  qubx/core/errors.py,sha256=LENtlgmVzxxUFNCsuy4PwyHYhkZkxuZQ2BPif8jaGmw,1411
66
66
  qubx/core/exceptions.py,sha256=11wQC3nnNLsl80zBqbE6xiKCqm31kctqo6W_gdnZkg8,581
67
67
  qubx/core/helpers.py,sha256=3rY7VkeL8WQ3hj7f8_rbSYNuQftlT0IDpTUARGP5ORM,21622
68
68
  qubx/core/initializer.py,sha256=VVti9UJDJCg0125Mm09S6Tt1foHK4XOBL0mXgDmvXes,7724
69
- qubx/core/interfaces.py,sha256=gSfOsYOlI-TSnt9fMRlO5-amsljq0ID50LPuwiR5HNA,66370
70
- qubx/core/loggers.py,sha256=pa28UYLTfRibhDzcbtPfbtNb3jpMZ8catTMikA0RFlc,14268
69
+ qubx/core/interfaces.py,sha256=AwSS92BbfUXw92TLvB75Wn5bxYByHAAXie2GAC70vUQ,67824
70
+ qubx/core/loggers.py,sha256=eYijsR02S5u1Hv21vjIk_dOUwOMv0fiBDYwEmFhAoWk,14344
71
71
  qubx/core/lookups.py,sha256=Bed30kPZvbTGjZ8exojhIMOIVfB46j6741yF3fXGTiM,18313
72
72
  qubx/core/metrics.py,sha256=eMsg9qaL86pkKVKrQOyF9u7yf27feLRxOvJj-6qwmj8,75551
73
73
  qubx/core/mixins/__init__.py,sha256=AMCLvfNuIb1kkQl3bhCj9jIOEl2eKcVPJeyLgrkB-rk,329
74
74
  qubx/core/mixins/market.py,sha256=w9OEDfy0r9xnb4KdKA-PuFsCQugNo4pMLQivGFeyqGw,6356
75
- qubx/core/mixins/processing.py,sha256=OgmrQfnUQKfHJ77BYrXQIu4AOz3xGlnuP8H6UvCiWhA,42216
75
+ qubx/core/mixins/processing.py,sha256=UqKwOzkDXvmcjvffrsR8vugq_XacwW9rcGM0TcBd9RM,44485
76
76
  qubx/core/mixins/subscription.py,sha256=GcZKHHzjPyYFLAEpE7j4fpLDOlAhFKojQEYfFO3QarY,11064
77
- qubx/core/mixins/trading.py,sha256=zq3JO8UDwbkYgmTHy1W8Ccn9Adeo1xw3vnyFAUFU0R4,9047
78
- qubx/core/mixins/universe.py,sha256=mzZJA7Me6HNFbAMGg1XOpnYCMtcFKHESTiozjaXyKXY,10100
77
+ qubx/core/mixins/trading.py,sha256=7KwxHiPWkXGnkHS4VLaxOZ7BHULkvvqPS8ooAG1NTxM,9477
78
+ qubx/core/mixins/universe.py,sha256=UBa3OIr2XvlK04O7YUG9c66CY8AZ5rQDSZov1rnUSjQ,10512
79
79
  qubx/core/mixins/utils.py,sha256=P71cLuqKjId8989MwOL_BtvvCnnwOFMkZyB1SY-0Ork,147
80
- qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=9cTUv-67TypYnfAjnTnUKc8tPt_ZMWXqoD2woFM7g3E,1019592
80
+ qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=0FPkdICMQaAvcLrcuMIV9RjdbyaD7Hq987H2awwlSiI,1019592
81
81
  qubx/core/series.pxd,sha256=PvnUEupOsZg8u81U5Amd-nbfmWQ0-PwZwc7yUoaZpoQ,4739
82
82
  qubx/core/series.pyi,sha256=RkM-F3AyrzT7m1H2UmOvZmmcOzU2eBeEWf2c0GUZe2o,5437
83
83
  qubx/core/series.pyx,sha256=wAn7L9HIkvVl-1Tt7bgdWhec7xy4AiHSXyDsrA4a29U,51703
84
84
  qubx/core/stale_data_detector.py,sha256=NHnnG9NkcivC93n8QMwJUzFVQv2ziUaN-fg76ppng_c,17118
85
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=SV1zNlv2BM1i-AaCyL7crSn6YNXDew_zG87n0ZoCXIk,86568
85
+ qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=yUmHzn-JTnvLybsw6nMLz-05T-4ITjc9H1EWNVFKQes,86568
86
86
  qubx/core/utils.pyi,sha256=a-wS13V2p_dM1CnGq40JVulmiAhixTwVwt0ah5By0Hc,348
87
87
  qubx/core/utils.pyx,sha256=UR9achMR-LARsztd2eelFsDsFH3n0gXACIKoGNPI9X4,1766
88
88
  qubx/data/__init__.py,sha256=BlyZ99esHLDmFA6ktNkuIce9RZO99TA1IMKWe94aI8M,599
@@ -96,10 +96,10 @@ qubx/emitters/__init__.py,sha256=MPs7ZRZZnURljusiuvlO5g8M4H1UjEfg5fkyKeJmIBI,791
96
96
  qubx/emitters/base.py,sha256=lxNmP81pXuRo0LKjjxkGqn0LCYjWDiqJ94dQdosGugg,8744
97
97
  qubx/emitters/composite.py,sha256=JkFch4Tp5q6CaLU2nAmeZnRiVPGkFhGNvzhT255yJfI,3411
98
98
  qubx/emitters/csv.py,sha256=S-oQ84rCgP-bb2_q-FWcegACg_Ej_Ik3tXE6aJBlqOk,4963
99
- qubx/emitters/indicator.py,sha256=4f5E9q8ofzQijJuaupRUpgVfq6fquzpa8sdNLtQJrlM,7972
99
+ qubx/emitters/indicator.py,sha256=NlhXJAZCboUDz7M7MOjfiR-ASM_L5qv0KgPJE-ekQCY,8206
100
100
  qubx/emitters/inmemory.py,sha256=AsFpAGGTWQsv42H5-3tDeZ3XP9b5Ye7lFHis53qcdjs,8862
101
101
  qubx/emitters/prometheus.py,sha256=lZJ_Hl-AlkeWJmktxhAiEMiTIc8dTQvBpf3Ih5Fy6pE,10516
102
- qubx/emitters/questdb.py,sha256=Zc4Vbr9g7Pw9oRSEnPnA1ypDv6nRwUEYAOrT-FFYFos,11526
102
+ qubx/emitters/questdb.py,sha256=hGneKVEnkV82t7c9G3_uVEgAoN302nXBSjjCWHKnAv0,11532
103
103
  qubx/exporters/__init__.py,sha256=7HeYHCZfKAaBVAByx9wE8DyGv6C55oeED9uUphcyjuc,360
104
104
  qubx/exporters/composite.py,sha256=c45XcMC0dsIDwOyOxxCuiyYQjUNhqPjptAulbaSqttU,2973
105
105
  qubx/exporters/formatters/__init__.py,sha256=La9rMsl3wyplza0xVyAFrUwhFyrGDIMJWmOB_boJyIg,488
@@ -145,18 +145,18 @@ qubx/resources/instruments/symbols-kraken-spot.json,sha256=3JLxi18nQAXE5J73hFY-m
145
145
  qubx/resources/instruments/symbols-kraken.f-future.json,sha256=FzOg8KIcl4nBQdPqugc-dMHxXGvyiQncNAHs84Tf4Pg,247468
146
146
  qubx/resources/instruments/symbols-kraken.f-perpetual.json,sha256=a1xXqbEcOyL1NLQO2JsSsseezPP7QCB9dub4IQhRviE,177233
147
147
  qubx/restarts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
148
- qubx/restarts/state_resolvers.py,sha256=n9c85uMlqwbtQ63eMJFdfnzmJx195rUhCB8VVc-Xe2Y,7539
148
+ qubx/restarts/state_resolvers.py,sha256=_MpcCqHL0_NL1A3CC9iKIwVffkjMHj6QJir3jQGrG10,7755
149
149
  qubx/restarts/time_finders.py,sha256=AX0Hb-RvB4YSot6L5m1ylkj09V7TYYXFvKgdb8gj0ok,4307
150
150
  qubx/restorers/__init__.py,sha256=vrnZBPJHR0-6knAccj4bK0tkjUPNRl32qiLr5Mv4aR0,911
151
151
  qubx/restorers/balance.py,sha256=yLV1vBki0XhBxrOhgaJBHuuL8VmIii82LAWgLxusbcE,6967
152
152
  qubx/restorers/factory.py,sha256=hQz3MRI7OH3SyMK1RjSuneb2ImJ_oWX0WSpu0tgJUug,6743
153
153
  qubx/restorers/interfaces.py,sha256=TsfdtcMUMncB6Cit_k66lEe5YKzVdVKpU68FXtZL-qY,1932
154
154
  qubx/restorers/position.py,sha256=jMJjq2ZJwHpAlG45bMy49WvkYK5UylDiExt7nVpxCfg,8703
155
- qubx/restorers/signal.py,sha256=7n7eeRhWGUBPbg179GxFH_ifywcl3pQJbwrcDklw0N0,14604
155
+ qubx/restorers/signal.py,sha256=5nK5ji8AucyWrFBK9uW619YCI_vPRGFnuDu8JnG3B_Y,14512
156
156
  qubx/restorers/state.py,sha256=I1VIN0ZcOjigc3WMHIYTNJeAAbN9YB21MDcMl04ZWmY,8018
157
157
  qubx/restorers/utils.py,sha256=We2gfqwQKWziUYhuUnjb-xo-5tSlbuHWpPQn0CEMTn0,1155
158
158
  qubx/ta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
159
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=ZcmtCMbF7wgX0bnTW5bOKfsQJT9M-yZjvS9rSSWqJ2A,762760
159
+ qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=xwgp8UOeeixa0CdddHwS_emo-39gCZ6TRSlI2u3ER8s,762760
160
160
  qubx/ta/indicators.pxd,sha256=l4JgvNdlWBuLqMzqTZVaYYn4XyZ9-c222YCyBXVv8MA,4843
161
161
  qubx/ta/indicators.pyi,sha256=kHoHONhzI7aq0qs-wP5cxyDPj96ZvQLlThEC8yQj6U4,2630
162
162
  qubx/ta/indicators.pyx,sha256=rT6OJ7xygZdTF4-pT6vr6g9MRhvbi6nYBlkTzzZYA_U,35126
@@ -204,15 +204,15 @@ qubx/utils/plotting/renderers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
204
204
  qubx/utils/plotting/renderers/plotly.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
205
205
  qubx/utils/questdb.py,sha256=bxlWiCyYf8IspsvXrs58tn5iXYBUtv6ojeYwOj8EXI0,5269
206
206
  qubx/utils/runner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
207
- qubx/utils/runner/_jupyter_runner.pyt,sha256=1bo06ql_wlZ7ng6go_zvemySzngrM8Uqzj-_xeOdiFg,10030
207
+ qubx/utils/runner/_jupyter_runner.pyt,sha256=DHXhXkjHe8-HkOa4g5EkSb3qbz64TLyM3-c__cQDPjk,9973
208
208
  qubx/utils/runner/accounts.py,sha256=mpiv6oxr5z97zWt7STYyARMhWQIpc_XFKungb_pX38U,3270
209
209
  qubx/utils/runner/configs.py,sha256=X915N6wbRSPFBiZ3WZcNQBSeoiocy-wVxCTTSdM8IAo,5367
210
210
  qubx/utils/runner/factory.py,sha256=hmtUDYNFQwVQffHEfxgrlmKwOGLcFQ6uJIH_ZLscpIY,16347
211
- qubx/utils/runner/runner.py,sha256=T2V6KSKcLNQVXYjxw4zrF7n_AhKEuHJBXjFGU0u1DQY,33252
211
+ qubx/utils/runner/runner.py,sha256=gZvj-ScJkSnbl7Vj3VENfdiruc5eCTzUkKek3zPmXiM,33299
212
212
  qubx/utils/time.py,sha256=xOWl_F6dOLFCmbB4xccLIx5yVt5HOH-I8ZcuowXjtBQ,11797
213
213
  qubx/utils/version.py,sha256=e52fIHyxzCiIuH7svCF6pkHuDlqL64rklqz-2XjWons,5309
214
- qubx-0.6.76.dist-info/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
215
- qubx-0.6.76.dist-info/METADATA,sha256=SPvvMbuvPf7wuoJ0fT_ZqBkx-yemYwZhJHpl4znfJ6s,5836
216
- qubx-0.6.76.dist-info/WHEEL,sha256=UckHTmFUCaLKpi4yFY8Dewu0c6XkY-KvEAGzGOnaWo8,110
217
- qubx-0.6.76.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
218
- qubx-0.6.76.dist-info/RECORD,,
214
+ qubx-0.6.78.dist-info/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
215
+ qubx-0.6.78.dist-info/METADATA,sha256=MdOcTFxgH5pUTsBloOTFMxpzZI3envIgDT7VrUzkOyE,5836
216
+ qubx-0.6.78.dist-info/WHEEL,sha256=UckHTmFUCaLKpi4yFY8Dewu0c6XkY-KvEAGzGOnaWo8,110
217
+ qubx-0.6.78.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
218
+ qubx-0.6.78.dist-info/RECORD,,
File without changes
File without changes