Qubx 0.6.64__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.65__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/data.py CHANGED
@@ -71,7 +71,7 @@ class SimulatedDataProvider(IDataProvider):
71
71
  # Check if the instrument was actually subscribed (not filtered out)
72
72
  if not self.has_subscription(i, subscription_type):
73
73
  continue
74
-
74
+
75
75
  h_data = self._data_source.peek_historical_data(i, subscription_type)
76
76
  if h_data:
77
77
  # _s_type = DataType.from_str(subscription_type)[0]
qubx/backtester/runner.py CHANGED
@@ -422,6 +422,10 @@ class SimulationRunner:
422
422
  logger.debug(f"[<y>simulator</y>] :: Setting default schedule: {self.data_config.default_trigger_schedule}")
423
423
  ctx.set_event_schedule(self.data_config.default_trigger_schedule)
424
424
 
425
+ if self.setup.enable_funding:
426
+ logger.debug("[<y>simulator</y>] :: Enabling funding rate simulation")
427
+ ctx.subscribe(DataType.FUNDING_PAYMENT)
428
+
425
429
  self.logs_writer = logs_writer
426
430
  self.channel = channel
427
431
  self.time_provider = simulated_clock
@@ -259,14 +259,14 @@ class IterableSimulationData(Iterator):
259
259
  def _filter_instruments_for_subscription(self, data_type: str, instruments: list[Instrument]) -> list[Instrument]:
260
260
  """
261
261
  Filter instruments based on subscription type requirements.
262
-
262
+
263
263
  For funding payment subscriptions, only SWAP instruments are supported since
264
264
  funding payments are specific to perpetual swap contracts.
265
-
265
+
266
266
  Args:
267
267
  data_type: The data type being subscribed to
268
268
  instruments: List of instruments to filter
269
-
269
+
270
270
  Returns:
271
271
  Filtered list of instruments appropriate for the subscription type
272
272
  """
@@ -275,23 +275,25 @@ class IterableSimulationData(Iterator):
275
275
  original_count = len(instruments)
276
276
  filtered_instruments = [i for i in instruments if i.market_type == MarketType.SWAP]
277
277
  filtered_count = len(filtered_instruments)
278
-
278
+
279
279
  # Log if instruments were filtered out (debug info)
280
280
  if filtered_count < original_count:
281
- logger.debug(f"Filtered {original_count - filtered_count} non-SWAP instruments from funding payment subscription")
282
-
281
+ logger.debug(
282
+ f"Filtered {original_count - filtered_count} non-SWAP instruments from funding payment subscription"
283
+ )
284
+
283
285
  return filtered_instruments
284
-
286
+
285
287
  # For all other subscription types, return instruments unchanged
286
288
  return instruments
287
289
 
288
290
  def add_instruments_for_subscription(self, subscription: str, instruments: list[Instrument] | Instrument):
289
291
  instruments = instruments if isinstance(instruments, list) else [instruments]
290
292
  _subt_key, _data_type, _params = self._parse_subscription_spec(subscription)
291
-
293
+
292
294
  # Filter instruments based on subscription type requirements
293
295
  instruments = self._filter_instruments_for_subscription(_data_type, instruments)
294
-
296
+
295
297
  # If no instruments remain after filtering, skip subscription
296
298
  if not instruments:
297
299
  return
@@ -4,6 +4,7 @@ import pandas as pd
4
4
  from joblib import delayed
5
5
 
6
6
  from qubx import QubxLogConfig, logger
7
+ from qubx.backtester.utils import SetupTypes
7
8
  from qubx.core.basics import Instrument
8
9
  from qubx.core.exceptions import SimulationError
9
10
  from qubx.core.metrics import TradingSessionResult
@@ -43,6 +44,7 @@ def simulate(
43
44
  accurate_stop_orders_execution: bool = False,
44
45
  signal_timeframe: str = "1Min",
45
46
  open_close_time_indent_secs=1,
47
+ enable_funding: bool = False,
46
48
  debug: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = "WARNING",
47
49
  show_latency_report: bool = False,
48
50
  portfolio_log_freq: str = "5Min",
@@ -70,6 +72,7 @@ def simulate(
70
72
  - accurate_stop_orders_execution (bool): If True, enables more accurate stop order execution simulation.
71
73
  - signal_timeframe (str): Timeframe for signals, default is "1Min".
72
74
  - open_close_time_indent_secs (int): Time indent in seconds for open/close times, default is 1.
75
+ - enable_funding (bool): If True, enables funding rate simulation, default is False.
73
76
  - debug (Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None): Logging level for debugging.
74
77
  - show_latency_report: If True, shows simulator's latency report.
75
78
  - portfolio_log_freq (str): Frequency for portfolio logging, default is "5Min".
@@ -114,6 +117,7 @@ def simulate(
114
117
  signal_timeframe=signal_timeframe,
115
118
  accurate_stop_orders_execution=accurate_stop_orders_execution,
116
119
  run_separate_instruments=run_separate_instruments,
120
+ enable_funding=enable_funding,
117
121
  )
118
122
  if not simulation_setups:
119
123
  logger.error(
@@ -204,6 +208,7 @@ def _run_setups(
204
208
  portfolio_log_freq,
205
209
  enable_inmemory_emitter,
206
210
  emitter_stats_interval,
211
+ close_data_readers=True,
207
212
  )
208
213
  for id, setup in enumerate(strategies_setups)
209
214
  )
@@ -219,6 +224,20 @@ def _run_setups(
219
224
  return successful_reports
220
225
 
221
226
 
227
+ def _adjust_start_date_for_min_instrument_onboard(setup: SimulationSetup, start: pd.Timestamp) -> pd.Timestamp:
228
+ """
229
+ Adjust the start date for the simulation to the onboard date of the instrument with the minimum onboard date.
230
+ """
231
+ onboard_dates = [
232
+ pd.Timestamp(instrument.onboard_date).replace(tzinfo=None)
233
+ for instrument in setup.instruments
234
+ if instrument.onboard_date is not None
235
+ ]
236
+ if not onboard_dates:
237
+ return start
238
+ return max(start, min(onboard_dates))
239
+
240
+
222
241
  def _run_setup(
223
242
  setup_id: int,
224
243
  account_id: str,
@@ -231,12 +250,18 @@ def _run_setup(
231
250
  portfolio_log_freq: str,
232
251
  enable_inmemory_emitter: bool = False,
233
252
  emitter_stats_interval: str = "1h",
253
+ close_data_readers: bool = False,
234
254
  ) -> TradingSessionResult | None:
235
255
  try:
236
256
  emitter = None
237
257
  emitter_data = None
238
258
  if enable_inmemory_emitter:
239
259
  emitter = InMemoryMetricEmitter(stats_interval=emitter_stats_interval)
260
+
261
+ # TODO: this can be removed once we add some artificial data stream to move the simulation
262
+ if setup.setup_type in [SetupTypes.SIGNAL, SetupTypes.SIGNAL_AND_TRACKER]:
263
+ start = _adjust_start_date_for_min_instrument_onboard(setup, start)
264
+
240
265
  runner = SimulationRunner(
241
266
  setup=setup,
242
267
  data_config=data_setup,
@@ -252,7 +277,7 @@ def _run_setup(
252
277
  level=QubxLogConfig.get_log_level(), custom_formatter=SimulatedLogFormatter(runner.ctx).formatter
253
278
  )
254
279
 
255
- runner.run(silent=silent)
280
+ runner.run(silent=silent, close_data_readers=close_data_readers)
256
281
 
257
282
  # - service latency report
258
283
  if show_latency_report:
qubx/backtester/utils.py CHANGED
@@ -90,6 +90,7 @@ class SimulationSetup:
90
90
  commissions: str | dict[str, str | None] | None = None
91
91
  signal_timeframe: str = "1Min"
92
92
  accurate_stop_orders_execution: bool = False
93
+ enable_funding: bool = False
93
94
 
94
95
  def __str__(self) -> str:
95
96
  return f"{self.name} {self.setup_type} capital {self.capital} {self.base_currency} for [{','.join(map(lambda x: x.symbol, self.instruments))}] @ {self.exchanges}[{self.commissions}]"
@@ -463,6 +464,7 @@ def recognize_simulation_configuration(
463
464
  signal_timeframe: str,
464
465
  accurate_stop_orders_execution: bool,
465
466
  run_separate_instruments: bool = False,
467
+ enable_funding: bool = False,
466
468
  ) -> list[SimulationSetup]:
467
469
  """
468
470
  Recognize and create setups based on the provided simulation configuration.
@@ -483,6 +485,7 @@ def recognize_simulation_configuration(
483
485
  - signal_timeframe (str): Timeframe for generated signals.
484
486
  - accurate_stop_orders_execution (bool): If True, enables more accurate stop order execution simulation.
485
487
  - run_separate_instruments (bool): If True, creates separate setups for each instrument.
488
+ - enable_funding (bool): If True, enables funding rate simulation, default is False.
486
489
 
487
490
  Returns:
488
491
  - list[SimulationSetup]: A list of SimulationSetup objects, each representing a
@@ -503,7 +506,8 @@ def recognize_simulation_configuration(
503
506
  r.extend(
504
507
  recognize_simulation_configuration(
505
508
  _n + n, v, instruments, exchanges, capital, basic_currency, commissions,
506
- signal_timeframe, accurate_stop_orders_execution, run_separate_instruments
509
+ signal_timeframe, accurate_stop_orders_execution, run_separate_instruments,
510
+ enable_funding
507
511
  )
508
512
  )
509
513
 
@@ -524,12 +528,14 @@ def recognize_simulation_configuration(
524
528
  if run_separate_instruments:
525
529
  # Create separate setups for each instrument
526
530
  for instrument in setup_instruments:
531
+ _s1 = c1[instrument.symbol] if isinstance(_s, pd.DataFrame) else _s
527
532
  r.append(
528
533
  SimulationSetup(
529
- _t, f"{name}/{instrument.symbol}", _s, c1, # type: ignore
534
+ _t, f"{name}/{instrument.symbol}", _s1, c1, # type: ignore
530
535
  [instrument],
531
536
  exchanges, capital, basic_currency, commissions,
532
- signal_timeframe, accurate_stop_orders_execution
537
+ signal_timeframe, accurate_stop_orders_execution,
538
+ enable_funding
533
539
  )
534
540
  )
535
541
  else:
@@ -538,7 +544,8 @@ def recognize_simulation_configuration(
538
544
  _t, name, _s, c1, # type: ignore
539
545
  setup_instruments,
540
546
  exchanges, capital, basic_currency, commissions,
541
- signal_timeframe, accurate_stop_orders_execution
547
+ signal_timeframe, accurate_stop_orders_execution,
548
+ enable_funding
542
549
  )
543
550
  )
544
551
  else:
@@ -547,7 +554,8 @@ def recognize_simulation_configuration(
547
554
  recognize_simulation_configuration(
548
555
  # name + "/" + str(j), s, instruments, exchange, capital, basic_currency, commissions
549
556
  name, s, instruments, exchanges, capital, basic_currency, commissions, # type: ignore
550
- signal_timeframe, accurate_stop_orders_execution, run_separate_instruments
557
+ signal_timeframe, accurate_stop_orders_execution, run_separate_instruments,
558
+ enable_funding
551
559
  )
552
560
  )
553
561
 
@@ -560,7 +568,8 @@ def recognize_simulation_configuration(
560
568
  SetupTypes.STRATEGY,
561
569
  f"{name}/{instrument.symbol}", configs, None, [instrument],
562
570
  exchanges, capital, basic_currency, commissions,
563
- signal_timeframe, accurate_stop_orders_execution
571
+ signal_timeframe, accurate_stop_orders_execution,
572
+ enable_funding
564
573
  )
565
574
  )
566
575
  else:
@@ -569,7 +578,8 @@ def recognize_simulation_configuration(
569
578
  SetupTypes.STRATEGY,
570
579
  name, configs, None, instruments,
571
580
  exchanges, capital, basic_currency, commissions,
572
- signal_timeframe, accurate_stop_orders_execution
581
+ signal_timeframe, accurate_stop_orders_execution,
582
+ enable_funding
573
583
  )
574
584
  )
575
585
 
@@ -581,12 +591,14 @@ def recognize_simulation_configuration(
581
591
  if run_separate_instruments:
582
592
  # Create separate setups for each instrument
583
593
  for instrument in setup_instruments:
594
+ _c1 = c1[instrument.symbol] if isinstance(c1, pd.DataFrame) else c1
584
595
  r.append(
585
596
  SimulationSetup(
586
597
  SetupTypes.SIGNAL,
587
- f"{name}/{instrument.symbol}", c1, None, [instrument],
598
+ f"{name}/{instrument.symbol}", _c1, None, [instrument],
588
599
  exchanges, capital, basic_currency, commissions,
589
- signal_timeframe, accurate_stop_orders_execution
600
+ signal_timeframe, accurate_stop_orders_execution,
601
+ enable_funding
590
602
  )
591
603
  )
592
604
  else:
@@ -595,7 +607,8 @@ def recognize_simulation_configuration(
595
607
  SetupTypes.SIGNAL,
596
608
  name, c1, None, setup_instruments,
597
609
  exchanges, capital, basic_currency, commissions,
598
- signal_timeframe, accurate_stop_orders_execution
610
+ signal_timeframe, accurate_stop_orders_execution,
611
+ enable_funding
599
612
  )
600
613
  )
601
614
 
qubx/core/metrics.py CHANGED
@@ -1131,11 +1131,28 @@ def find_session(sessions: list[TradingSessionResult], name: str) -> TradingSess
1131
1131
  raise ValueError(f"Session with name {name} not found")
1132
1132
 
1133
1133
 
1134
- def find_sessions(sessions: list[TradingSessionResult], name: str) -> list[TradingSessionResult]:
1134
+ def find_sessions(sessions: list[TradingSessionResult], *names: str) -> list[TradingSessionResult]:
1135
1135
  """
1136
- Match the session by a regex pattern. It can also be a substring.
1136
+ Match sessions by regex patterns or substrings. Returns sessions that match at least one of the provided names.
1137
+
1138
+ Args:
1139
+ sessions: List of TradingSessionResult objects to search through
1140
+ *names: One or more name patterns to match against
1141
+
1142
+ Returns:
1143
+ List of sessions where the name matches at least one of the provided patterns
1137
1144
  """
1138
- return [s for s in sessions if re.match(name, s.name) or name in s.name]
1145
+ if not names:
1146
+ return []
1147
+
1148
+ matched_sessions = []
1149
+ for s in sessions:
1150
+ for name in names:
1151
+ if re.match(name, s.name) or name in s.name:
1152
+ matched_sessions.append(s)
1153
+ break # Don't add the same session multiple times
1154
+
1155
+ return matched_sessions
1139
1156
 
1140
1157
 
1141
1158
  def tearsheet(
@@ -1403,6 +1420,90 @@ def calculate_leverage(
1403
1420
  return (value.squeeze() / capital).mul(100).rename("Leverage") # type: ignore
1404
1421
 
1405
1422
 
1423
+ def calculate_leverage_per_symbol(
1424
+ session: TradingSessionResult, start: str | pd.Timestamp | None = None
1425
+ ) -> pd.DataFrame:
1426
+ """
1427
+ Calculate leverage for each symbol in the trading session.
1428
+
1429
+ Args:
1430
+ session: TradingSessionResult containing portfolio data and capital info
1431
+ start: Optional start timestamp for calculation (defaults to session start)
1432
+
1433
+ Returns:
1434
+ pd.DataFrame with columns for each symbol showing their leverage percentage over time
1435
+ """
1436
+ portfolio = session.portfolio_log
1437
+ init_capital = session.get_total_capital()
1438
+ start = start or session.start
1439
+
1440
+ # Calculate total capital (same for all symbols)
1441
+ total_pnl = calculate_total_pnl(portfolio, split_cumulative=False).loc[start:]
1442
+ capital = init_capital + total_pnl["Total_PnL"].cumsum() - total_pnl["Total_Commissions"].cumsum()
1443
+
1444
+ # Extract unique symbols from column names
1445
+ value_columns = [col for col in portfolio.columns if "_Value" in col]
1446
+ symbols = sorted(list(set(col.split("_")[0] for col in value_columns)))
1447
+
1448
+ # Calculate leverage for each symbol
1449
+ leverages = {}
1450
+ for symbol in symbols:
1451
+ value = portfolio.filter(regex=f"{symbol}_Value").loc[start:].sum(axis=1)
1452
+ if not value.empty:
1453
+ leverages[symbol] = (value.squeeze() / capital).mul(100)
1454
+
1455
+ return pd.DataFrame(leverages)
1456
+
1457
+
1458
+ def calculate_pnl_per_symbol(
1459
+ session: TradingSessionResult,
1460
+ include_commissions: bool = True,
1461
+ pct_from_initial_capital: bool = True,
1462
+ start: str | pd.Timestamp | None = None,
1463
+ ) -> pd.DataFrame:
1464
+ """
1465
+ Calculate PnL for each symbol in the trading session.
1466
+
1467
+ Args:
1468
+ session: TradingSessionResult containing portfolio data
1469
+ cumulative: If True, return cumulative PnL; if False, return per-period PnL
1470
+ include_commissions: If True, subtract commissions from PnL
1471
+ start: Optional start timestamp for calculation (defaults to session start)
1472
+
1473
+ Returns:
1474
+ pd.DataFrame with columns for each symbol showing their PnL over time
1475
+ """
1476
+ portfolio = session.portfolio_log
1477
+ start = start or session.start
1478
+ init_capital = session.get_total_capital()
1479
+
1480
+ # Extract unique symbols from PnL columns
1481
+ pnl_columns = [col for col in portfolio.columns if "_PnL" in col]
1482
+ symbols = sorted(list(set(col.split("_")[0] for col in pnl_columns)))
1483
+
1484
+ # Calculate PnL for each symbol
1485
+ pnls = {}
1486
+ for symbol in symbols:
1487
+ # Get PnL for this symbol
1488
+ symbol_pnl = portfolio.filter(regex=f"{symbol}_PnL").loc[start:]
1489
+
1490
+ if not symbol_pnl.empty:
1491
+ pnl_series = symbol_pnl.squeeze()
1492
+
1493
+ if include_commissions:
1494
+ # Subtract commissions if requested
1495
+ symbol_comms = portfolio.filter(regex=f"{symbol}_Commissions").loc[start:]
1496
+ if not symbol_comms.empty:
1497
+ comm_series = symbol_comms.squeeze()
1498
+ pnl_series = pnl_series - comm_series
1499
+
1500
+ pnls[symbol] = pnl_series.cumsum()
1501
+ if pct_from_initial_capital:
1502
+ pnls[symbol] = round(pnls[symbol] / init_capital * 100, 2)
1503
+
1504
+ return pd.DataFrame(pnls)
1505
+
1506
+
1406
1507
  def chart_signals(
1407
1508
  result: TradingSessionResult,
1408
1509
  symbol: str,
@@ -1546,14 +1647,18 @@ def get_symbol_pnls(
1546
1647
  return pd.DataFrame(pnls, index=[s.name for s in session])
1547
1648
 
1548
1649
 
1549
- def combine_sessions(sessions: list[TradingSessionResult], name: str = "Portfolio") -> TradingSessionResult:
1650
+ def combine_sessions(
1651
+ sessions: list[TradingSessionResult], name: str = "Portfolio", scale_capital: bool = True
1652
+ ) -> TradingSessionResult:
1550
1653
  """
1551
1654
  DEPRECATED: use extend_trading_results instead
1552
1655
  """
1553
1656
  session = copy(sessions[0])
1554
1657
  session.name = name
1555
1658
  session.instruments = list(set(chain.from_iterable([e.instruments for e in sessions])))
1556
- session.capital = sessions[0].get_total_capital() * len(sessions)
1659
+ session.capital = sessions[0].get_total_capital()
1660
+ if scale_capital:
1661
+ session.capital *= len(sessions)
1557
1662
  session.portfolio_log = pd.concat(
1558
1663
  [e.portfolio_log.loc[:, (e.portfolio_log != 0).any(axis=0)] for e in sessions], axis=1
1559
1664
  )
@@ -540,10 +540,10 @@ class ProcessingManager(IProcessingManager):
540
540
  current_time - self._last_data_ready_log_time
541
541
  ) >= td_64(10, "s")
542
542
  if should_log:
543
- logger.warning(
544
- f"No instruments ready after timeout - still waiting "
545
- f"({ready_instruments}/{total_instruments} ready)"
546
- )
543
+ # logger.warning(
544
+ # f"No instruments ready after timeout - still waiting "
545
+ # f"({ready_instruments}/{total_instruments} ready)"
546
+ # )
547
547
  self._last_data_ready_log_time = current_time
548
548
  return False
549
549
 
@@ -734,10 +734,12 @@ class ProcessingManager(IProcessingManager):
734
734
  base_update = self.__update_base_data(instrument, event_type, quote)
735
735
  return MarketEvent(self._time_provider.time(), event_type, instrument, quote, is_trigger=base_update)
736
736
 
737
- def _handle_funding_payment(self, instrument: Instrument, event_type: str, funding_payment: FundingPayment) -> MarketEvent:
737
+ def _handle_funding_payment(
738
+ self, instrument: Instrument, event_type: str, funding_payment: FundingPayment
739
+ ) -> MarketEvent:
738
740
  # Apply funding payment to position
739
741
  self._account.process_funding_payment(instrument, funding_payment)
740
-
742
+
741
743
  # Continue with existing event processing
742
744
  base_update = self.__update_base_data(instrument, event_type, funding_payment)
743
745
  return MarketEvent(self._time_provider.time(), event_type, instrument, funding_payment, is_trigger=base_update)
qubx/data/readers.py CHANGED
@@ -1107,9 +1107,13 @@ def _calculate_max_candles_chunksize(timeframe: str | None) -> int:
1107
1107
  # Convert timeframe to pandas Timedelta
1108
1108
  tf_delta = pd.Timedelta(timeframe)
1109
1109
 
1110
- # Calculate how many candles fit in 1 week
1111
- one_week = pd.Timedelta("7d")
1112
- max_candles = int(one_week / tf_delta)
1110
+ if timeframe == "1d":
1111
+ one_month = pd.Timedelta("30d")
1112
+ max_candles = int(one_month / tf_delta)
1113
+ else:
1114
+ # Calculate how many candles fit in 1 week
1115
+ one_week = pd.Timedelta("7d")
1116
+ max_candles = int(one_week / tf_delta)
1113
1117
 
1114
1118
  # Ensure we don't return 0 or negative values
1115
1119
  return max(1, max_candles)
@@ -1152,6 +1156,11 @@ def _calculate_time_windows_for_chunking(
1152
1156
  windows.append((current_start, current_end))
1153
1157
  current_start = current_end
1154
1158
 
1159
+ # If last window is less than half of the window before it, then merge together
1160
+ if len(windows) > 1 and windows[-1][0] - windows[-1][1] < chunk_duration / 2:
1161
+ windows[-2] = (windows[-2][0], windows[-1][1])
1162
+ windows.pop()
1163
+
1155
1164
  return windows
1156
1165
  except (ValueError, TypeError):
1157
1166
  # If timeframe can't be parsed, fall back to single window
@@ -1353,6 +1362,12 @@ class QuestDBConnector(DataReader):
1353
1362
  self._connection = pg.connect(self.connection_url, autocommit=True)
1354
1363
  logger.debug(f"Connected to QuestDB at {self._host}:{self._port}")
1355
1364
 
1365
+ def close(self):
1366
+ if self._connection:
1367
+ self._connection.close()
1368
+ self._connection = None
1369
+ logger.debug(f"Disconnected from QuestDB at {self._host}:{self._port}")
1370
+
1356
1371
  def read(
1357
1372
  self,
1358
1373
  data_id: str,
qubx/trackers/__init__.py CHANGED
@@ -5,6 +5,7 @@ __all__ = [
5
5
  "FixedRiskSizer",
6
6
  "FixedLeverageSizer",
7
7
  "LongShortRatioPortfolioSizer",
8
+ "InverseVolatilitySizer",
8
9
  "FixedRiskSizerWithConstantCapital",
9
10
  "ImprovedEntryTracker",
10
11
  "ImprovedEntryTrackerDynamicTake",
@@ -26,5 +27,6 @@ from .sizers import (
26
27
  FixedRiskSizer,
27
28
  FixedRiskSizerWithConstantCapital,
28
29
  FixedSizer,
30
+ InverseVolatilitySizer,
29
31
  LongShortRatioPortfolioSizer,
30
32
  )
qubx/trackers/sizers.py CHANGED
@@ -1,8 +1,24 @@
1
1
  import numpy as np
2
+ import pandas as pd
2
3
 
3
4
  from qubx import logger
4
5
  from qubx.core.basics import Signal, TargetPosition
5
6
  from qubx.core.interfaces import IPositionSizer, IStrategyContext
7
+ from qubx.ta.indicators import atr
8
+ from qubx.utils.time import infer_series_frequency
9
+
10
+ _S_YEAR = 24 * 3600 * 365
11
+
12
+
13
+ def annual_factor(tframe_or_series: str | pd.Series) -> float:
14
+ timeframe = (
15
+ infer_series_frequency(tframe_or_series[:25]) if isinstance(tframe_or_series, pd.Series) else tframe_or_series
16
+ )
17
+ return _S_YEAR / pd.Timedelta(timeframe).total_seconds()
18
+
19
+
20
+ def annual_factor_sqrt(tframe_or_series: str | pd.Series) -> float:
21
+ return np.sqrt(annual_factor(tframe_or_series))
6
22
 
7
23
 
8
24
  class FixedSizer(IPositionSizer):
@@ -231,3 +247,43 @@ class FixedRiskSizerWithConstantCapital(IPositionSizer):
231
247
  t_pos.append(signal.target_for_amount(target_position_size))
232
248
 
233
249
  return t_pos
250
+
251
+
252
+ class InverseVolatilitySizer(IPositionSizer):
253
+ def __init__(
254
+ self,
255
+ target_risk: float,
256
+ atr_timeframe: str = "4h",
257
+ atr_period: int = 40,
258
+ atr_smoother: str = "sma",
259
+ divide_by_universe_size: bool = False,
260
+ ) -> None:
261
+ self.target_risk = target_risk
262
+ self.atr_timeframe = atr_timeframe
263
+ self.atr_period = atr_period
264
+ self.atr_smoother = atr_smoother
265
+ self.divide_by_universe_size = divide_by_universe_size
266
+
267
+ def calculate_target_positions(self, ctx: IStrategyContext, signals: list[Signal]) -> list[TargetPosition]:
268
+ return [self._get_target_position(ctx, signal) for signal in signals]
269
+
270
+ def _get_target_position(self, ctx: IStrategyContext, signal: Signal) -> TargetPosition:
271
+ _ohlc = ctx.ohlc(signal.instrument, self.atr_timeframe, length=self.atr_period * 2)
272
+ _atr = atr(_ohlc, self.atr_period, self.atr_smoother, percentage=True)
273
+ if len(_atr) == 0 or np.isnan(_atr[1]):
274
+ return signal.target_for_amount(0)
275
+
276
+ _ann_vol_fraction = (_atr[1] * annual_factor_sqrt(self.atr_timeframe)) / 100.0
277
+ if np.isclose(_ann_vol_fraction, 0):
278
+ return signal.target_for_amount(0)
279
+
280
+ universe_size = len(ctx.instruments)
281
+ price = _ohlc[0].close
282
+ size = (
283
+ ctx.get_total_capital()
284
+ * (self.target_risk / _ann_vol_fraction)
285
+ * signal.signal
286
+ / (universe_size if self.divide_by_universe_size else 1)
287
+ / price
288
+ )
289
+ return signal.target_for_amount(size)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: Qubx
3
- Version: 0.6.64
3
+ Version: 0.6.65
4
4
  Summary: Qubx - Quantitative Trading Framework
5
5
  Author: Dmitry Marienko
6
6
  Author-email: dmitry.marienko@xlydian.com
@@ -3,15 +3,15 @@ qubx/_nb_magic.py,sha256=G3LkaX_-gN5Js6xl7rjaahSs_u3dVPDRCZW0IIxPCb0,3051
3
3
  qubx/backtester/__init__.py,sha256=OhXhLmj2x6sp6k16wm5IPATvv-E2qRZVIcvttxqPgcg,176
4
4
  qubx/backtester/account.py,sha256=0yvE06icSeK2ymovvaKkuftY8Ou3Z7Y2JrDa6VtkINw,3048
5
5
  qubx/backtester/broker.py,sha256=JMasxycLqCT99NxN50uyQ1uxtpHYL0wpp4sJ3hB6v2M,2688
6
- qubx/backtester/data.py,sha256=kbSXwMYz1fEmX2eltw7fR7m7gSrCYOQJOZ7kFPNj7Ss,6883
6
+ qubx/backtester/data.py,sha256=ZRvgMPXyM0t6tYQ18pPMfBtxbzrNPfvMgj0W7AY3txo,6867
7
7
  qubx/backtester/management.py,sha256=FQSMkdrTZrxKdLRrf4Uiw60pdBMb0xESeFrTfH9AqZk,20713
8
8
  qubx/backtester/ome.py,sha256=LnnSANMD2XBo18JtLRh96Ey9BH_StrfshQnCu2_aOc4,18646
9
9
  qubx/backtester/optimization.py,sha256=HHUIYA6Y66rcOXoePWFOuOVX9iaHGKV0bGt_4d5e6FM,7619
10
- qubx/backtester/runner.py,sha256=AzH9-mFpFncrWsdZrkctzyQSMpahn_SOfdtBqm0NVVw,20504
11
- qubx/backtester/simulated_data.py,sha256=wa_9Y-eqR8F7VuCYJxrC1o7N_lBTDvDbRMP_PGO1_HY,19132
10
+ qubx/backtester/runner.py,sha256=_QWaH-11-SaLAqRj3VYT3xKIkohDPqaz2niveMejUDI,20678
11
+ qubx/backtester/simulated_data.py,sha256=wgesN8mKMiIBDbLtFZUh6YDf1sHGC8-ilLCPIlfRXLg,19094
12
12
  qubx/backtester/simulated_exchange.py,sha256=Xg0yv21gq4q9CeCeZoupcenNEBORrxpb93ONZEGL2xk,8076
13
- qubx/backtester/simulator.py,sha256=8DLCMxkLSQWmKeFEiC1a_3Tkyc1NFn6Q9JLh0vx97U4,11924
14
- qubx/backtester/utils.py,sha256=iJNEvpKKZzUoiD_albE-sfvLdWYhYKSBGdrCWZCTx1k,36550
13
+ qubx/backtester/simulator.py,sha256=HAhkiUIztl7Q0zGtp1seThJDa85yBBm2gNlDjmlDgjw,13036
14
+ qubx/backtester/utils.py,sha256=f0SrcNI2ZBEwxQ04HqxLeoOJ4ycb29g_uiITnoztt2s,37192
15
15
  qubx/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  qubx/cli/commands.py,sha256=MdiBGb8Z3mAFDq44Dn4EuSQRk2aArIrfo1GJ-VwgeQQ,8197
17
17
  qubx/cli/deploy.py,sha256=pQ9FPOsywDyy8jOjLfrgYTTkKQ-MCixCzbgsG68Q3_0,8319
@@ -46,25 +46,25 @@ qubx/core/initializer.py,sha256=YgTBs5LpIk6ZFdmMD8zCnJnVNcMh1oeYvt157jhwyMg,4242
46
46
  qubx/core/interfaces.py,sha256=vsz9Bo-UapQVsPQ7jz6Oec4OHoUtgx0fEeAMqY9-GKg,62464
47
47
  qubx/core/loggers.py,sha256=pa28UYLTfRibhDzcbtPfbtNb3jpMZ8catTMikA0RFlc,14268
48
48
  qubx/core/lookups.py,sha256=2-yjMkijRY2jRTnT8hH4n5Lq2m9z9oBtyBPF8vwxKlc,18313
49
- qubx/core/metrics.py,sha256=p5QJS-7raHdHB1blajR0fS9PT06yC8yf-ckyVxYMlPE,62277
49
+ qubx/core/metrics.py,sha256=DlDynG_YnfnY0s7AKhyWLMcgUPJZpN4SUUFadjbc9Tk,66054
50
50
  qubx/core/mixins/__init__.py,sha256=AMCLvfNuIb1kkQl3bhCj9jIOEl2eKcVPJeyLgrkB-rk,329
51
51
  qubx/core/mixins/market.py,sha256=my4RvQIfvecIiqM-UzHJteEYakWl9JwhmgQ8OaGstN4,5173
52
- qubx/core/mixins/processing.py,sha256=WLPQFLt1vNgdi5-zWDO3bvYAxoJU9PUEU_0Q_lQhHP0,37817
52
+ qubx/core/mixins/processing.py,sha256=-VL3m5rZcH96MfyXs_ZntoWayPoDDL2Lz5GSE2RX-lA,37831
53
53
  qubx/core/mixins/subscription.py,sha256=V_g9wCPQ8S5SHkU-qOZ84cV5nReAUrV7DoSNAGG0LPY,10372
54
54
  qubx/core/mixins/trading.py,sha256=idfRPaqrvkfMxzu9mXr9i_xfqLee-ZAOrERxkxv6Ruo,7256
55
55
  qubx/core/mixins/universe.py,sha256=mzZJA7Me6HNFbAMGg1XOpnYCMtcFKHESTiozjaXyKXY,10100
56
- qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=mt7vIjFPAjQqoUlT1RwBs0_U1U_WVmwGOfDcHQ87ylo,994760
56
+ qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=EPZz0HPC7YLX8dPkrJVjB0h3cnxepXWHxk_y05Yh0NY,994760
57
57
  qubx/core/series.pxd,sha256=aI5PG1hbr827xwcnSYgGMF2IBD4GvCRby_i9lrGrJdQ,4026
58
58
  qubx/core/series.pyi,sha256=AHROxA9wer3W2ehgI08wPpDVc4gPkGbzvmkkA4K60Ts,4821
59
59
  qubx/core/series.pyx,sha256=w7XtvAHPnov0Twov9c-xdhgQARVbSrTiIVi1Axo2VWQ,47925
60
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=gCA9wCLC3Rb-KM3DDZCb_HhArSA93xA0RXljpZsvt-Y,86568
60
+ qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=tNu7GL0pTNl1dz86aNMkVYAh77wLwCNSQ6IBnSmvxI8,86568
61
61
  qubx/core/utils.pyi,sha256=a-wS13V2p_dM1CnGq40JVulmiAhixTwVwt0ah5By0Hc,348
62
62
  qubx/core/utils.pyx,sha256=UR9achMR-LARsztd2eelFsDsFH3n0gXACIKoGNPI9X4,1766
63
63
  qubx/data/__init__.py,sha256=ELZykvpPGWc5rX7QoNyNQwMLgdKMG8MACOByA4pM5hA,549
64
64
  qubx/data/composite.py,sha256=nLA3w3kMvzOc8n2rjAZ35sKa5DXAe8SOFsEov8PpES4,18881
65
65
  qubx/data/helpers.py,sha256=GtU9fUJoqJiKiOJA6I6j2I7ikauT2jtkfMNryFsEMpE,17859
66
66
  qubx/data/hft.py,sha256=be7AwzTOjqqCENn0ClrZoHDyKv3SFG66IyTp8QadHlM,33687
67
- qubx/data/readers.py,sha256=0QIBA2w5sgyd0IAdo4cEIrYzQ3XvikuKHK4LgiVmdbQ,73800
67
+ qubx/data/readers.py,sha256=FCrhQDfclQBZgoKMDY83z7j2Ga7dw44cf1D37VAxxpc,74420
68
68
  qubx/data/registry.py,sha256=SqCZ9Q0BZiHW2gC9yRuiVRV0lejyJAHI-694Yl_Cfdo,3892
69
69
  qubx/data/tardis.py,sha256=O-zglpusmO6vCY3arSOgH6KUbkfPajSAIQfMKlVmh_E,33878
70
70
  qubx/emitters/__init__.py,sha256=MPs7ZRZZnURljusiuvlO5g8M4H1UjEfg5fkyKeJmIBI,791
@@ -131,16 +131,16 @@ qubx/restorers/signal.py,sha256=7n7eeRhWGUBPbg179GxFH_ifywcl3pQJbwrcDklw0N0,1460
131
131
  qubx/restorers/state.py,sha256=I1VIN0ZcOjigc3WMHIYTNJeAAbN9YB21MDcMl04ZWmY,8018
132
132
  qubx/restorers/utils.py,sha256=We2gfqwQKWziUYhuUnjb-xo-5tSlbuHWpPQn0CEMTn0,1155
133
133
  qubx/ta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
134
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=ZnRDooOV-j5vEOJIoitWHRRRnpdFTyjGpjR0C7boFS8,662632
134
+ qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=VuZOYiQho7aPAASSulbHLYMtnOEltI1AbwaCNDgr-fg,662632
135
135
  qubx/ta/indicators.pxd,sha256=Goo0_N0Xnju8XGo3Xs-3pyg2qr_0Nh5C-_26DK8U_IE,4224
136
136
  qubx/ta/indicators.pyi,sha256=19W0uERft49In5bf9jkJHkzJYEyE9gzudN7_DJ5Vdv8,1963
137
137
  qubx/ta/indicators.pyx,sha256=Xgpew46ZxSXsdfSEWYn3A0Q35MLsopB9n7iyCsXTufs,25969
138
- qubx/trackers/__init__.py,sha256=ThIP1jXaACse5hG3lZqQSlWSKYl6APxFmBHaRcVpPdU,1002
138
+ qubx/trackers/__init__.py,sha256=zvIahF8MwSffBMOX2BDFFNKJCWtL8TyFLQLL_DR22JM,1060
139
139
  qubx/trackers/advanced.py,sha256=CONogr5hHHEwfolkegjpEz7NNk4Ruf9pOq5nKifNXVM,12761
140
140
  qubx/trackers/composite.py,sha256=Tjupx78SraXmRKkWhu8n81RkPjOgsDbXLd8yz6PhbaA,6318
141
141
  qubx/trackers/rebalancers.py,sha256=KFY7xuD4fGALiSLMas8MZ3ueRzlWt5wDT9329dlmNng,5150
142
142
  qubx/trackers/riskctrl.py,sha256=O6UTk4nK7u5YaT_Sd4aSFBR3dWaxOLLzOzMoeU71hDY,35022
143
- qubx/trackers/sizers.py,sha256=OEK-IOuXiXXx8MkZiEsni5zPWFc3kun9AqApY0mIPTY,9527
143
+ qubx/trackers/sizers.py,sha256=u9HBcjyEdtYquDUXUaxW5bSI7mHWfJpM09WmvmM0w0k,11631
144
144
  qubx/utils/__init__.py,sha256=FEPBtU3dhfLawBkAfm9FEUW4RuOY7pGCBfzDCtKjn9A,481
145
145
  qubx/utils/_pyxreloader.py,sha256=34kNd8kQi2ey_ZrGdVVUHbPrO1PEiHZDLEDBscIkT_s,12292
146
146
  qubx/utils/charting/lookinglass.py,sha256=m7lWU8c0E8tXzGbkN0GB8CL-kd92MnH_wD8cATX067k,39232
@@ -168,8 +168,8 @@ qubx/utils/runner/factory.py,sha256=eM4-Etcq-FewD2AjH_srFGzP413pm8er95KIZixXRpM,
168
168
  qubx/utils/runner/runner.py,sha256=m58a3kEwSs1xfgg_s9FwrQJ3AZV4Lf_VOmKDPQdaWH8,31518
169
169
  qubx/utils/time.py,sha256=qZVLcvp0AkG0JOuDiIgUk4znO8PUHWNEpMwr3z-ko7E,10170
170
170
  qubx/utils/version.py,sha256=e52fIHyxzCiIuH7svCF6pkHuDlqL64rklqz-2XjWons,5309
171
- qubx-0.6.64.dist-info/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
172
- qubx-0.6.64.dist-info/METADATA,sha256=WRALA5A5n7pSTy_0UZ7Jz4XJCqPYt2i-nFtT782lcEk,4612
173
- qubx-0.6.64.dist-info/WHEEL,sha256=UckHTmFUCaLKpi4yFY8Dewu0c6XkY-KvEAGzGOnaWo8,110
174
- qubx-0.6.64.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
175
- qubx-0.6.64.dist-info/RECORD,,
171
+ qubx-0.6.65.dist-info/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
172
+ qubx-0.6.65.dist-info/METADATA,sha256=o41M90rwzMsTL2X50Zg5vXtKn8erii6vL1H1pOI9Xb4,4612
173
+ qubx-0.6.65.dist-info/WHEEL,sha256=UckHTmFUCaLKpi4yFY8Dewu0c6XkY-KvEAGzGOnaWo8,110
174
+ qubx-0.6.65.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
175
+ qubx-0.6.65.dist-info/RECORD,,
File without changes
File without changes