Qubx 0.6.85__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.87__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.

@@ -327,10 +327,11 @@ class BacktestsResultsManager:
327
327
  if not as_table:
328
328
  print(_s)
329
329
 
330
+ dd_column = "max_dd_pct" if "max_dd_pct" in metrics else "mdd_pct"
330
331
  if with_metrics:
331
332
  _m_repr = (
332
333
  pd.DataFrame.from_dict(metrics, orient="index")
333
- .T[["gain", "cagr", "sharpe", "qr", "max_dd_pct", "mdd_usd", "fees", "execs"]]
334
+ .T[["gain", "cagr", "sharpe", "qr", dd_column, "mdd_usd", "fees", "execs"]]
334
335
  .astype(float)
335
336
  )
336
337
  _m_repr = _m_repr.round(3).to_string(index=False)
@@ -345,7 +346,7 @@ class BacktestsResultsManager:
345
346
  metrics = {
346
347
  m: round(v, 3)
347
348
  for m, v in metrics.items()
348
- if m in ["gain", "cagr", "sharpe", "qr", "max_dd_pct", "mdd_usd", "fees", "execs"]
349
+ if m in ["gain", "cagr", "sharpe", "qr", dd_column, "mdd_usd", "fees", "execs"]
349
350
  }
350
351
  _t_rep.append(
351
352
  {"Index": info.get("idx", ""), "Strategy": name}
qubx/backtester/runner.py CHANGED
@@ -478,7 +478,7 @@ class SimulationRunner:
478
478
  )
479
479
 
480
480
  if self.emitter is not None:
481
- self.emitter.set_time_provider(simulated_clock)
481
+ self.emitter.set_context(ctx)
482
482
 
483
483
  # - setup base subscription from spec
484
484
  if ctx.get_base_subscription() == DataType.NONE:
qubx/cli/commands.py CHANGED
@@ -137,6 +137,51 @@ def ls(directory: str):
137
137
  ls_strats(directory)
138
138
 
139
139
 
140
+ @main.command()
141
+ @click.argument("config-file", type=Path, required=True)
142
+ @click.option(
143
+ "--no-check-imports",
144
+ is_flag=True,
145
+ default=False,
146
+ help="Skip checking if strategy class can be imported",
147
+ show_default=True,
148
+ )
149
+ def validate(config_file: Path, no_check_imports: bool):
150
+ """
151
+ Validates a strategy configuration file without running it.
152
+
153
+ Checks for:
154
+ - Valid YAML syntax
155
+ - Required configuration fields
156
+ - Strategy class exists and can be imported (unless --no-check-imports)
157
+ - Exchange configurations are valid
158
+ - Simulation parameters are valid (if present)
159
+
160
+ Returns exit code 0 if valid, 1 if invalid.
161
+ """
162
+ from qubx.utils.runner.configs import validate_strategy_config
163
+
164
+ result = validate_strategy_config(config_file, check_imports=not no_check_imports)
165
+
166
+ if result.valid:
167
+ click.echo(click.style("✓ Configuration is valid", fg="green", bold=True))
168
+ if result.warnings:
169
+ click.echo(click.style("\nWarnings:", fg="yellow", bold=True))
170
+ for warning in result.warnings:
171
+ click.echo(click.style(f" - {warning}", fg="yellow"))
172
+ raise SystemExit(0)
173
+ else:
174
+ click.echo(click.style("✗ Configuration is invalid", fg="red", bold=True))
175
+ click.echo(click.style("\nErrors:", fg="red", bold=True))
176
+ for error in result.errors:
177
+ click.echo(click.style(f" - {error}", fg="red"))
178
+ if result.warnings:
179
+ click.echo(click.style("\nWarnings:", fg="yellow", bold=True))
180
+ for warning in result.warnings:
181
+ click.echo(click.style(f" - {warning}", fg="yellow"))
182
+ raise SystemExit(1)
183
+
184
+
140
185
  @main.command()
141
186
  @click.argument(
142
187
  "directory",
@@ -358,7 +403,7 @@ def init(
358
403
  The generated strategy can be run immediately with:
359
404
  poetry run qubx run --config config.yml --paper
360
405
  """
361
- from qubx.templates import TemplateManager, TemplateError
406
+ from qubx.templates import TemplateError, TemplateManager
362
407
 
363
408
  try:
364
409
  manager = TemplateManager()
@@ -8,7 +8,7 @@ from ...adapters.polling_adapter import PollingConfig, PollingToWebSocketAdapter
8
8
  from ..base import CcxtFuturePatchMixin
9
9
 
10
10
  # Constants
11
- FUNDING_RATE_DEFAULT_POLL_MINUTES = 5
11
+ FUNDING_RATE_DEFAULT_POLL_MINUTES = 1
12
12
  FUNDING_RATE_HOUR_MS = 60 * 60 * 1000 # 1 hour in milliseconds
13
13
 
14
14
 
@@ -71,7 +71,7 @@ class FundingRateDataHandler(BaseDataTypeHandler):
71
71
  channel.send((instrument, DataType.FUNDING_RATE, funding_rate, False))
72
72
 
73
73
  # Emit payment if funding interval changed
74
- if self._should_emit_payment(instrument, funding_rate):
74
+ if self._should_emit_payment(instrument, funding_rate, current_time):
75
75
  payment = self._create_funding_payment(instrument)
76
76
  channel.send((instrument, DataType.FUNDING_PAYMENT, payment, False))
77
77
 
@@ -101,7 +101,7 @@ class FundingRateDataHandler(BaseDataTypeHandler):
101
101
  stream_name=name,
102
102
  )
103
103
 
104
- def _should_emit_payment(self, instrument: Instrument, rate: FundingRate) -> bool:
104
+ def _should_emit_payment(self, instrument: Instrument, rate: FundingRate, current_time: dt_64) -> bool:
105
105
  """
106
106
  Determine if a funding payment should be emitted.
107
107
 
@@ -132,7 +132,7 @@ class FundingRateDataHandler(BaseDataTypeHandler):
132
132
  return False
133
133
 
134
134
  # Emit if next_funding_time has advanced (new funding period started)
135
- if rate.next_funding_time > last_info["payment_time"]:
135
+ if rate.next_funding_time > last_info["payment_time"] and current_time > last_info["payment_time"]:
136
136
  # Store payment info for _create_funding_payment
137
137
  self._pending_funding_rates[f"{key}_payment"] = {
138
138
  "rate": last_info["rate"].rate,
@@ -20,7 +20,7 @@ from .utils import ccxt_find_instrument, instrument_to_ccxt_symbol
20
20
 
21
21
  @reader("ccxt")
22
22
  class CcxtDataReader(DataReader):
23
- SUPPORTED_DATA_TYPES = {"ohlc", "funding_payment"}
23
+ SUPPORTED_DATA_TYPES = {"ohlc"}
24
24
 
25
25
  _exchanges: dict[str, Exchange]
26
26
  _loop: AsyncThreadLoop
@@ -74,7 +74,8 @@ class CcxtDataReader(DataReader):
74
74
  if instrument is None:
75
75
  return []
76
76
 
77
- _timeframe = pd.Timedelta(timeframe or "1m")
77
+ timeframe = timeframe or "1m"
78
+ _timeframe = pd.Timedelta(timeframe)
78
79
  _start, _stop = self._get_start_stop(start, stop, _timeframe)
79
80
 
80
81
  if _start > _stop:
qubx/core/interfaces.py CHANGED
@@ -2050,7 +2050,7 @@ class IMetricEmitter:
2050
2050
  self,
2051
2051
  name: str,
2052
2052
  value: float,
2053
- tags: dict[str, str] | None = None,
2053
+ tags: dict[str, Any] | None = None,
2054
2054
  timestamp: dt_64 | None = None,
2055
2055
  instrument: Instrument | None = None,
2056
2056
  ) -> None:
@@ -2092,15 +2092,16 @@ class IMetricEmitter:
2092
2092
  """
2093
2093
  pass
2094
2094
 
2095
- def set_time_provider(self, time_provider: ITimeProvider) -> None:
2095
+ def set_context(self, context: "IStrategyContext") -> None:
2096
2096
  """
2097
- Set the time provider for the metric emitter.
2097
+ Set the strategy context for the metric emitter.
2098
2098
 
2099
- This method is used to set the time provider that will be used to get timestamps
2100
- when no explicit timestamp is provided in the emit method.
2099
+ This method is used to set the context that provides access to time and simulation state.
2100
+ The context is used to automatically add is_live tag and get timestamps when no explicit
2101
+ timestamp is provided in the emit method.
2101
2102
 
2102
2103
  Args:
2103
- time_provider: The time provider to use
2104
+ context: The strategy context to use
2104
2105
  """
2105
2106
  pass
2106
2107
 
qubx/core/metrics.py CHANGED
@@ -175,7 +175,7 @@ def cagr(returns, periods=DAILY):
175
175
 
176
176
  cumrets = (returns + 1).cumprod(axis=0)
177
177
  years = len(cumrets) / float(periods)
178
- return (cumrets.iloc[-1] ** (1.0 / years)) - 1.0
178
+ return ((cumrets.iloc[-1] ** (1.0 / years)) - 1.0) * 100
179
179
 
180
180
 
181
181
  def calmar_ratio(returns, periods=DAILY):
@@ -747,6 +747,11 @@ class TradingSessionResult:
747
747
  """Get number of executions"""
748
748
  return len(self.executions_log)
749
749
 
750
+ @property
751
+ def turnover(self) -> float:
752
+ """Get average daily turnover as percentage of equity"""
753
+ return self.performance().get("avg_daily_turnover", 0.0)
754
+
750
755
  @property
751
756
  def leverage(self) -> pd.Series:
752
757
  """Get leverage over time"""
@@ -779,7 +784,7 @@ class TradingSessionResult:
779
784
  for k in [
780
785
  "equity", "drawdown_usd", "drawdown_pct",
781
786
  "compound_returns", "returns_daily", "returns", "monthly_returns",
782
- "rolling_sharpe", "long_value", "short_value",
787
+ "rolling_sharpe", "long_value", "short_value", "turnover",
783
788
  ]:
784
789
  self._metrics.pop(k, None)
785
790
  # fmt: on
@@ -1381,16 +1386,21 @@ def portfolio_metrics(
1381
1386
  execs = len(executions_log)
1382
1387
  mdd_pct = 100 * dd_data / equity.cummax() if execs > 0 else pd.Series(0, index=equity.index)
1383
1388
  sheet["equity"] = equity
1384
- sheet["gain"] = sheet["equity"].iloc[-1] - sheet["equity"].iloc[0]
1385
- sheet["cagr"] = cagr(returns_daily, performance_statistics_period)
1386
1389
  sheet["sharpe"] = sharpe_ratio(returns_daily, risk_free, performance_statistics_period)
1390
+ sheet["cagr"] = cagr(returns_daily, performance_statistics_period)
1391
+
1392
+ # turnover calculation
1393
+ symbols = list(set(portfolio_log.columns.str.split("_").str.get(0).values))
1394
+ turnover_series = calculate_turnover(portfolio_log, symbols, equity, resample="1d")
1395
+ sheet["turnover"] = turnover_series
1396
+ sheet["daily_turnover"] = turnover_series.mean() if len(turnover_series) > 0 else 0.0
1397
+
1387
1398
  sheet["qr"] = qr(equity) if execs > 0 else 0
1388
- sheet["drawdown_usd"] = dd_data
1399
+ sheet["mdd_pct"] = max(mdd_pct)
1389
1400
  sheet["drawdown_pct"] = mdd_pct
1401
+ sheet["drawdown_usd"] = dd_data
1390
1402
  # 25-May-2019: MDE fixed Max DD pct calculations
1391
- sheet["max_dd_pct"] = max(mdd_pct)
1392
1403
  # sheet["max_dd_pct_on_init"] = 100 * mdd / init_cash
1393
- sheet["mdd_usd"] = mdd
1394
1404
  sheet["mdd_start"] = equity.index[ddstart]
1395
1405
  sheet["mdd_peak"] = equity.index[ddpeak]
1396
1406
  sheet["mdd_recover"] = equity.index[ddrecover]
@@ -1403,12 +1413,12 @@ def portfolio_metrics(
1403
1413
  )
1404
1414
  sheet["calmar"] = calmar_ratio(returns_daily, performance_statistics_period)
1405
1415
  # sheet["ann_vol"] = annual_volatility(returns_daily)
1406
- sheet["tail_ratio"] = tail_ratio(returns_daily)
1407
- sheet["stability"] = stability_of_returns(returns_daily)
1416
+ # sheet["tail_ratio"] = tail_ratio(returns_daily)
1417
+ # sheet["stability"] = stability_of_returns(returns_daily)
1408
1418
  sheet["monthly_returns"] = aggregate_returns(returns_daily, convert_to="mon")
1409
1419
  r_m = np.mean(returns_daily)
1410
1420
  r_s = np.std(returns_daily)
1411
- sheet["var"] = var_cov_var(init_cash, r_m, r_s)
1421
+ # sheet["var"] = var_cov_var(init_cash, r_m, r_s)
1412
1422
  sheet["avg_return"] = 100 * r_m
1413
1423
 
1414
1424
  # portfolio market values
@@ -1416,6 +1426,8 @@ def portfolio_metrics(
1416
1426
  sheet["long_value"] = mkt_value[mkt_value > 0].sum(axis=1).fillna(0)
1417
1427
  sheet["short_value"] = mkt_value[mkt_value < 0].sum(axis=1).fillna(0)
1418
1428
 
1429
+ sheet["gain"] = sheet["equity"].iloc[-1] - sheet["equity"].iloc[0]
1430
+ sheet["mdd_usd"] = mdd
1419
1431
  # total commissions
1420
1432
  sheet["fees"] = pft_total["Total_Commissions"].iloc[-1]
1421
1433
 
@@ -1423,9 +1435,10 @@ def portfolio_metrics(
1423
1435
  funding_columns = pft_total.filter(regex=".*_Funding")
1424
1436
  if not funding_columns.empty:
1425
1437
  total_funding = funding_columns.sum(axis=1)
1426
- sheet["funding_pnl"] = 100 * total_funding.iloc[-1] / init_cash # as percentage of initial capital
1427
- else:
1428
- sheet["funding_pnl"] = 0.0
1438
+ if total_funding.iloc[-1] != 0:
1439
+ sheet["funding_pnl"] = 100 * total_funding.iloc[-1] / init_cash # as percentage of initial capital
1440
+ # else:
1441
+ # sheet["funding_pnl"] = 0.0
1429
1442
 
1430
1443
  # executions metrics
1431
1444
  sheet["execs"] = execs
@@ -1725,7 +1738,7 @@ def _tearsheet_single(
1725
1738
  ay = sbp(_n, 5)
1726
1739
  plt.plot(lev, c="c", lw=1.5, label="Leverage")
1727
1740
  plt.subplots_adjust(hspace=0)
1728
- return pd.DataFrame(report).T.round(3)
1741
+ return pd.DataFrame(report).T.round(2)
1729
1742
 
1730
1743
 
1731
1744
  def calculate_leverage(
@@ -1828,6 +1841,53 @@ def calculate_pnl_per_symbol(
1828
1841
  return df
1829
1842
 
1830
1843
 
1844
+ def calculate_turnover(
1845
+ portfolio_log: pd.DataFrame,
1846
+ symbols: list[str],
1847
+ equity: pd.Series,
1848
+ resample: str = "1d",
1849
+ ) -> pd.Series:
1850
+ """
1851
+ Calculate daily turnover as percentage of equity.
1852
+
1853
+ Turnover measures trading activity by calculating the absolute value of position changes
1854
+ multiplied by price, then dividing by equity.
1855
+
1856
+ Args:
1857
+ portfolio_log: Portfolio log dataframe with position and price columns
1858
+ symbols: List of symbols to calculate turnover for
1859
+ equity: Equity curve series
1860
+ resample: Resampling period for turnover calculation (default "1d")
1861
+
1862
+ Returns:
1863
+ pd.Series: Daily turnover as percentage of equity
1864
+ """
1865
+ position_diffs = []
1866
+
1867
+ for symbol in symbols:
1868
+ pos_col = f"{symbol}_Pos"
1869
+ price_col = f"{symbol}_Price"
1870
+
1871
+ if pos_col in portfolio_log.columns and price_col in portfolio_log.columns:
1872
+ # Calculate absolute position change multiplied by price (notional value)
1873
+ position_diff = portfolio_log[pos_col].diff().abs() * portfolio_log[price_col]
1874
+ position_diffs.append(position_diff)
1875
+
1876
+ if not position_diffs:
1877
+ return pd.Series(0, index=equity.index)
1878
+
1879
+ # Sum all position changes and resample to specified period
1880
+ notional_turnover = pd.concat(position_diffs, axis=1).sum(axis=1).resample(resample).sum()
1881
+
1882
+ # Resample equity to match turnover frequency
1883
+ equity_resampled = equity.resample(resample).last()
1884
+
1885
+ # Calculate turnover as percentage of equity
1886
+ daily_turnover = notional_turnover.div(equity_resampled).mul(100).fillna(0)
1887
+
1888
+ return daily_turnover
1889
+
1890
+
1831
1891
  def chart_signals(
1832
1892
  result: TradingSessionResult,
1833
1893
  symbol: str,
qubx/emitters/base.py CHANGED
@@ -4,13 +4,13 @@ Base Metric Emitter.
4
4
  This module provides a base implementation of IMetricEmitter that can be extended by other emitters.
5
5
  """
6
6
 
7
- from typing import Dict, List, Optional, Set
7
+ from typing import Any, Dict, List, Optional, Set
8
8
 
9
9
  import pandas as pd
10
10
 
11
11
  from qubx import logger
12
12
  from qubx.core.basics import Instrument, Signal, TargetPosition, dt_64
13
- from qubx.core.interfaces import IAccountViewer, IMetricEmitter, IStrategyContext, ITimeProvider
13
+ from qubx.core.interfaces import IAccountViewer, IMetricEmitter, IStrategyContext
14
14
 
15
15
 
16
16
  class BaseMetricEmitter(IMetricEmitter):
@@ -35,7 +35,7 @@ class BaseMetricEmitter(IMetricEmitter):
35
35
  }
36
36
 
37
37
  def __init__(
38
- self, stats_to_emit: Optional[List[str]] = None, stats_interval: str = "1m", tags: dict[str, str] | None = None
38
+ self, stats_to_emit: Optional[List[str]] = None, stats_interval: str = "1m", tags: dict[str, Any] | None = None
39
39
  ):
40
40
  """
41
41
  Initialize the Base Metric Emitter.
@@ -49,18 +49,19 @@ class BaseMetricEmitter(IMetricEmitter):
49
49
  self._stats_interval = pd.Timedelta(stats_interval)
50
50
  self._default_tags = tags or {}
51
51
  self._last_emission_time = None
52
- self._time_provider = None
52
+ self._context = None
53
53
 
54
- def _merge_tags(self, tags: dict[str, str] | None = None, instrument: Instrument | None = None) -> dict[str, str]:
54
+ def _merge_tags(self, tags: dict[str, Any] | None = None, instrument: Instrument | None = None) -> dict[str, Any]:
55
55
  """
56
56
  Merge default tags with provided tags and instrument tags if provided.
57
+ Also automatically adds is_live tag based on context's simulation state.
57
58
 
58
59
  Args:
59
60
  tags: Additional tags to merge with default tags
60
61
  instrument: Optional instrument to add symbol and exchange tags from
61
62
 
62
63
  Returns:
63
- Dict[str, str]: Merged tags dictionary
64
+ Dict[str, Any]: Merged tags dictionary
64
65
  """
65
66
  result = self._default_tags.copy()
66
67
 
@@ -70,9 +71,13 @@ class BaseMetricEmitter(IMetricEmitter):
70
71
  if instrument:
71
72
  result.update({"symbol": instrument.symbol, "exchange": instrument.exchange})
72
73
 
74
+ # Add is_live tag based on context's simulation state
75
+ if self._context is not None:
76
+ result["is_live"] = not self._context.is_simulation
77
+
73
78
  return result
74
79
 
75
- def _emit_impl(self, name: str, value: float, tags: Dict[str, str], timestamp: dt_64 | None = None) -> None:
80
+ def _emit_impl(self, name: str, value: float, tags: Dict[str, Any], timestamp: dt_64 | None = None) -> None:
76
81
  """
77
82
  Implementation of emit to be overridden by subclasses.
78
83
 
@@ -88,7 +93,7 @@ class BaseMetricEmitter(IMetricEmitter):
88
93
  self,
89
94
  name: str,
90
95
  value: float,
91
- tags: dict[str, str] | None = None,
96
+ tags: dict[str, Any] | None = None,
92
97
  timestamp: dt_64 | None = None,
93
98
  instrument: Instrument | None = None,
94
99
  ) -> None:
@@ -102,19 +107,19 @@ class BaseMetricEmitter(IMetricEmitter):
102
107
  timestamp: Optional timestamp for the metric (defaults to current time)
103
108
  instrument: Optional instrument to add symbol and exchange tags from
104
109
  """
105
- if self._time_provider is not None and timestamp is None:
106
- timestamp = self._time_provider.time()
110
+ if self._context is not None and timestamp is None:
111
+ timestamp = self._context.time()
107
112
  merged_tags = self._merge_tags(tags, instrument)
108
113
  self._emit_impl(name, float(value), merged_tags, timestamp)
109
114
 
110
- def set_time_provider(self, time_provider: ITimeProvider) -> None:
115
+ def set_context(self, context: IStrategyContext) -> None:
111
116
  """
112
- Set the time provider for the metric emitter.
117
+ Set the strategy context for the metric emitter.
113
118
 
114
119
  Args:
115
- time_provider: The time provider to use
120
+ context: The strategy context to use
116
121
  """
117
- self._time_provider = time_provider
122
+ self._context = context
118
123
 
119
124
  def emit_strategy_stats(self, context: IStrategyContext) -> None:
120
125
  """
@@ -126,6 +131,10 @@ class BaseMetricEmitter(IMetricEmitter):
126
131
  Args:
127
132
  context: The strategy context to get statistics from
128
133
  """
134
+ # Store context to ensure is_live tag is added
135
+ if self._context is None:
136
+ self._context = context
137
+
129
138
  try:
130
139
  # Get current timestamp
131
140
  current_time = context.time()
@@ -85,6 +85,19 @@ class CompositeMetricEmitter(BaseMetricEmitter):
85
85
  except Exception as e:
86
86
  logger.error(f"Error emitting signals to {emitter.__class__.__name__}: {e}")
87
87
 
88
+ def set_context(self, context: IStrategyContext) -> None:
89
+ """
90
+ Set the strategy context for all child emitters.
91
+
92
+ Args:
93
+ context: The strategy context to use
94
+ """
95
+ for emitter in self._emitters:
96
+ try:
97
+ emitter.set_context(context)
98
+ except Exception as e:
99
+ logger.error(f"Error setting context on {emitter.__class__.__name__}: {e}")
100
+
88
101
  def notify(self, context: IStrategyContext) -> None:
89
102
  for emitter in self._emitters:
90
103
  try:
qubx/emitters/csv.py CHANGED
@@ -6,6 +6,7 @@ This module provides an implementation of IMetricEmitter that exports metrics to
6
6
 
7
7
  import os
8
8
  from pathlib import Path
9
+ from typing import Any
9
10
 
10
11
  from qubx import logger
11
12
  from qubx.core.basics import Signal, dt_64
@@ -27,7 +28,7 @@ class CSVMetricEmitter(BaseMetricEmitter):
27
28
  file_path: str | None = None,
28
29
  stats_to_emit: list[str] | None = None,
29
30
  stats_interval: str = "1m",
30
- tags: dict[str, str] | None = None,
31
+ tags: dict[str, Any] | None = None,
31
32
  ):
32
33
  """
33
34
  Initialize the CSV Metric Emitter.
@@ -5,6 +5,8 @@ This module provides the IndicatorEmitter class that can wrap around any indicat
5
5
  and automatically emit their values when there are updates.
6
6
  """
7
7
 
8
+ from typing import Any
9
+
8
10
  import numpy as np
9
11
  import pandas as pd
10
12
 
@@ -43,7 +45,7 @@ class IndicatorEmitter(Indicator):
43
45
  metric_emitter: IMetricEmitter,
44
46
  metric_name: str | None = None,
45
47
  instrument: Instrument | None = None,
46
- tags: dict[str, str] | None = None,
48
+ tags: dict[str, Any] | None = None,
47
49
  emit_on_new_item_only: bool = True,
48
50
  ):
49
51
  """
@@ -149,7 +151,7 @@ class IndicatorEmitter(Indicator):
149
151
  metric_emitter: IMetricEmitter,
150
152
  metric_name: str | None = None,
151
153
  instrument: Instrument | None = None,
152
- tags: dict[str, str] | None = None,
154
+ tags: dict[str, Any] | None = None,
153
155
  emit_on_new_item_only: bool = True,
154
156
  ) -> "Indicator":
155
157
  """
qubx/emitters/inmemory.py CHANGED
@@ -5,7 +5,7 @@ This module provides an implementation of IMetricEmitter that stores metrics in
5
5
  using a pandas DataFrame for easy access and analysis.
6
6
  """
7
7
 
8
- from typing import Optional
8
+ from typing import Any, cast
9
9
 
10
10
  import pandas as pd
11
11
 
@@ -28,7 +28,7 @@ class InMemoryMetricEmitter(BaseMetricEmitter):
28
28
  self,
29
29
  stats_to_emit: list[str] | None = None,
30
30
  stats_interval: str = "1m",
31
- tags: dict[str, str] | None = None,
31
+ tags: dict[str, Any] | None = None,
32
32
  max_rows: int | None = None,
33
33
  ):
34
34
  """
@@ -135,7 +135,7 @@ class InMemoryMetricEmitter(BaseMetricEmitter):
135
135
  if not self._rows:
136
136
  df = pd.DataFrame(columns=["timestamp", "name", "value", "symbol", "exchange"])
137
137
  else:
138
- df = pd.DataFrame(self._rows)
138
+ df = pd.DataFrame(self._rows.copy())
139
139
  # Ensure correct dtypes
140
140
  df = df.astype(
141
141
  {
@@ -163,7 +163,8 @@ class InMemoryMetricEmitter(BaseMetricEmitter):
163
163
  df = df[df["timestamp"] >= start_time]
164
164
  if end_time is not None:
165
165
  df = df[df["timestamp"] <= end_time]
166
- return df
166
+
167
+ return cast(pd.DataFrame, df)
167
168
 
168
169
  def get_latest_metrics(
169
170
  self, instrument: Instrument | None = None, symbol: str | None = None, exchange: str | None = None
@@ -4,7 +4,7 @@ Prometheus Metric Emitter.
4
4
  This module provides an implementation of IMetricEmitter that exports metrics to Prometheus.
5
5
  """
6
6
 
7
- from typing import Dict, List, Literal, Optional
7
+ from typing import Any, Dict, List, Literal, Optional
8
8
 
9
9
  from prometheus_client import REGISTRY, Counter, Gauge, Summary, push_to_gateway
10
10
 
@@ -178,7 +178,7 @@ class PrometheusMetricEmitter(BaseMetricEmitter):
178
178
  self,
179
179
  name: str,
180
180
  value: float,
181
- tags: dict[str, str] | None = None,
181
+ tags: dict[str, Any] | None = None,
182
182
  timestamp: dt_64 | None = None,
183
183
  metric_type: MetricType = "gauge",
184
184
  ) -> None:
qubx/emitters/questdb.py CHANGED
@@ -6,6 +6,7 @@ This module provides an implementation of IMetricEmitter that exports metrics to
6
6
 
7
7
  import datetime
8
8
  from concurrent.futures import ThreadPoolExecutor
9
+ from typing import Any
9
10
 
10
11
  import pandas as pd
11
12
  from questdb.ingress import Sender
@@ -24,6 +25,8 @@ class QuestDBMetricEmitter(BaseMetricEmitter):
24
25
  This emitter sends metrics to QuestDB with custom timestamps and tags.
25
26
  """
26
27
 
28
+ SYMBOL_TAGS = ["symbol", "exchange", "type", "environment", "strategy"]
29
+
27
30
  def __init__(
28
31
  self,
29
32
  host: str = "localhost",
@@ -33,7 +36,7 @@ class QuestDBMetricEmitter(BaseMetricEmitter):
33
36
  stats_to_emit: list[str] | None = None,
34
37
  stats_interval: str = "1m",
35
38
  flush_interval: str = "5s",
36
- tags: dict[str, str] | None = None,
39
+ tags: dict[str, Any] | None = None,
37
40
  max_workers: int = 1,
38
41
  ):
39
42
  """
@@ -143,10 +146,8 @@ class QuestDBMetricEmitter(BaseMetricEmitter):
143
146
  return
144
147
 
145
148
  # Prepare symbols (tags) and columns (values)
146
- symbols = {"metric_name": name}
147
- symbols.update(tags) # Add all tags as symbols
148
-
149
- columns: dict = {"value": round(value, 5)} # Add the value as a column
149
+ symbols = self._pop_symbols(tags)
150
+ columns: dict = {"metric_name": name, "value": round(value, 5), **tags}
150
151
 
151
152
  # Use the provided timestamp if available, otherwise use current time
152
153
  dt_timestamp = self._convert_timestamp(timestamp) if timestamp is not None else datetime.datetime.now()
@@ -261,11 +262,7 @@ class QuestDBMetricEmitter(BaseMetricEmitter):
261
262
 
262
263
  # Use _merge_tags to get properly merged tags
263
264
  merged_tags = self._merge_tags({}, signal.instrument)
264
-
265
- symbols = {
266
- "group_name": signal.group if signal.group else "",
267
- }
268
- symbols.update(merged_tags) # Add merged tags
265
+ symbols = self._pop_symbols(merged_tags)
269
266
 
270
267
  columns = {
271
268
  "signal": float(signal.signal),
@@ -277,6 +274,8 @@ class QuestDBMetricEmitter(BaseMetricEmitter):
277
274
  "comment": signal.comment if signal.comment else "",
278
275
  # "options": json.dumps(signal.options) if signal.options else "{}",
279
276
  "is_service": bool(signal.is_service),
277
+ "group_name": signal.group if signal.group else "",
278
+ **merged_tags,
280
279
  }
281
280
 
282
281
  # Convert timestamp - signal.time is always dt_64, no need to check for string
@@ -287,3 +286,10 @@ class QuestDBMetricEmitter(BaseMetricEmitter):
287
286
 
288
287
  except Exception as e:
289
288
  logger.error(f"[QuestDBMetricEmitter] Failed to emit signals to QuestDB: {e}")
289
+
290
+ def _pop_symbols(self, tags: dict[str, str]) -> dict[str, str]:
291
+ symbols = {}
292
+ for symbol_name in self.SYMBOL_TAGS:
293
+ if symbol_name in tags:
294
+ symbols[symbol_name] = tags.pop(symbol_name)
295
+ return symbols
@@ -8,5 +8,12 @@ before it is exported to external systems.
8
8
  from qubx.exporters.formatters.base import DefaultFormatter, IExportFormatter
9
9
  from qubx.exporters.formatters.incremental import IncrementalFormatter
10
10
  from qubx.exporters.formatters.slack import SlackMessageFormatter
11
+ from qubx.exporters.formatters.target_position import TargetPositionFormatter
11
12
 
12
- __all__ = ["IExportFormatter", "DefaultFormatter", "SlackMessageFormatter", "IncrementalFormatter"]
13
+ __all__ = [
14
+ "IExportFormatter",
15
+ "DefaultFormatter",
16
+ "SlackMessageFormatter",
17
+ "IncrementalFormatter",
18
+ "TargetPositionFormatter",
19
+ ]
@@ -0,0 +1,78 @@
1
+ from typing import Any, Optional
2
+
3
+ import numpy as np
4
+
5
+ from qubx.core.basics import TargetPosition, dt_64
6
+ from qubx.core.interfaces import IAccountViewer
7
+ from qubx.exporters.formatters.base import DefaultFormatter
8
+
9
+
10
+ class TargetPositionFormatter(DefaultFormatter):
11
+ """
12
+ Formatter for exporting target positions as structured messages.
13
+
14
+ This formatter creates messages suitable for trading systems that need
15
+ to know target position sizes with leverage calculations.
16
+ """
17
+
18
+ def __init__(
19
+ self,
20
+ alert_name: str,
21
+ exchange_mapping: Optional[dict[str, str]] = None,
22
+ ):
23
+ """
24
+ Initialize the TargetPositionFormatter.
25
+
26
+ Args:
27
+ alert_name: The name of the alert to include in the messages
28
+ exchange_mapping: Optional mapping of exchange names to use in messages.
29
+ If an exchange is not in the mapping, the instrument's exchange is used.
30
+ """
31
+ super().__init__()
32
+ self.alert_name = alert_name
33
+ self.exchange_mapping = exchange_mapping or {}
34
+
35
+ def format_target_position(self, time: dt_64, target: TargetPosition, account: IAccountViewer) -> dict[str, Any]:
36
+ """
37
+ Format a target position for export.
38
+
39
+ This method creates a structured message with target position information
40
+ including leverage calculated as (notional size / total capital).
41
+
42
+ Args:
43
+ time: Timestamp when the target position was generated
44
+ target: The target position to format
45
+ account: Account viewer to get account information like total capital, leverage, etc.
46
+
47
+ Returns:
48
+ A dictionary containing the formatted target position data
49
+ """
50
+ # Get price: use entry_price if available, else fallback to position's last_update_price
51
+ price = target.entry_price
52
+ if price is None:
53
+ position = account.get_position(target.instrument)
54
+ if not np.isnan(position.last_update_price):
55
+ price = position.last_update_price
56
+ else:
57
+ # Cannot calculate leverage without price
58
+ return {}
59
+
60
+ # Calculate notional size and leverage
61
+ notional = abs(target.target_position_size * price)
62
+ total_capital = account.get_total_capital(exchange=target.instrument.exchange)
63
+ leverage = notional / total_capital if total_capital > 0 else 0.0
64
+
65
+ # Determine side
66
+ side = "BUY" if target.target_position_size > 0 else "SELL"
67
+
68
+ # Get the exchange name from mapping or use the instrument's exchange
69
+ exchange = self.exchange_mapping.get(target.instrument.exchange, target.instrument.exchange)
70
+
71
+ return {
72
+ "action": "TARGET_POSITION",
73
+ "alertName": self.alert_name,
74
+ "exchange": exchange,
75
+ "symbol": target.instrument.exchange_symbol.upper(),
76
+ "side": side,
77
+ "leverage": leverage,
78
+ }
qubx/health/base.py CHANGED
@@ -7,7 +7,7 @@ import numpy as np
7
7
 
8
8
  from qubx import logger
9
9
  from qubx.core.basics import CtrlChannel, dt_64
10
- from qubx.core.interfaces import HealthMetrics, IHealthMonitor, IMetricEmitter, ITimeProvider, IDataArrivalListener
10
+ from qubx.core.interfaces import HealthMetrics, IDataArrivalListener, IHealthMonitor, IMetricEmitter, ITimeProvider
11
11
  from qubx.core.utils import recognize_timeframe
12
12
  from qubx.utils.collections import DequeFloat64, DequeIndicator
13
13
 
@@ -111,8 +111,6 @@ class DummyHealthMonitor(IHealthMonitor, IDataArrivalListener):
111
111
  """Stop the health metrics monitor."""
112
112
  pass
113
113
 
114
-
115
-
116
114
  def watch(self, name: str = ""):
117
115
  """No-op decorator function that returns the function unchanged.
118
116
 
@@ -136,6 +134,7 @@ class BaseHealthMonitor(IHealthMonitor, IDataArrivalListener):
136
134
  channel: CtrlChannel | None = None,
137
135
  queue_monitor_interval: str = "100ms",
138
136
  buffer_size: int = 1000,
137
+ emit_health: bool = True,
139
138
  ):
140
139
  """Initialize the health metrics monitor.
141
140
 
@@ -145,10 +144,13 @@ class BaseHealthMonitor(IHealthMonitor, IDataArrivalListener):
145
144
  emit_interval: Interval to emit metrics, e.g. "1s", "500ms", "5m" (default: "1s")
146
145
  channel: Optional data channel to monitor for queue size
147
146
  queue_monitor_interval: Interval to check queue size, e.g. "100ms", "500ms" (default: "100ms")
147
+ buffer_size: Size of buffer for storing metrics
148
+ emit_health: Whether to emit health metrics (default: True)
148
149
  """
149
150
  self.time_provider = time_provider
150
151
  self._emitter = emitter
151
152
  self._channel = channel
153
+ self._emit_health = emit_health
152
154
 
153
155
  # Convert emit interval to nanoseconds
154
156
  self._emit_interval_ns = recognize_timeframe(emit_interval)
@@ -384,8 +386,6 @@ class BaseHealthMonitor(IHealthMonitor, IDataArrivalListener):
384
386
  p99_processing_latency=p99_processing_latency,
385
387
  )
386
388
 
387
-
388
-
389
389
  def start(self) -> None:
390
390
  """Start the metrics emission thread and queue monitoring thread."""
391
391
  # Start queue size monitoring if channel is provided
@@ -396,6 +396,7 @@ class BaseHealthMonitor(IHealthMonitor, IDataArrivalListener):
396
396
 
397
397
  # Start metrics emission if emitter is provided
398
398
  if self._emitter is not None:
399
+
399
400
  def emit_metrics():
400
401
  while not self._stop_event.is_set():
401
402
  try:
@@ -408,13 +409,10 @@ class BaseHealthMonitor(IHealthMonitor, IDataArrivalListener):
408
409
  self._stop_event.clear()
409
410
  self._emission_thread = threading.Thread(target=emit_metrics, daemon=True)
410
411
  self._emission_thread.start()
411
-
412
-
413
412
 
414
413
  def stop(self) -> None:
415
414
  """Stop the metrics emission thread and queue monitoring thread."""
416
415
 
417
-
418
416
  # Stop queue size monitoring
419
417
  if self._monitor_thread is not None:
420
418
  self._is_running = False
@@ -440,7 +438,6 @@ class BaseHealthMonitor(IHealthMonitor, IDataArrivalListener):
440
438
  finally:
441
439
  time.sleep(self._queue_monitor_interval_s)
442
440
 
443
-
444
441
  def _get_latency_percentile(self, event_type: str, latencies: dict, percentile: float) -> float:
445
442
  if event_type not in latencies or latencies[event_type].is_empty():
446
443
  return 0.0
@@ -449,7 +446,7 @@ class BaseHealthMonitor(IHealthMonitor, IDataArrivalListener):
449
446
 
450
447
  def _emit(self) -> None:
451
448
  """Emit all metrics to the configured emitter."""
452
- if self._emitter is None:
449
+ if not self._emit_health or self._emitter is None:
453
450
  return
454
451
 
455
452
  metrics = self.get_system_metrics()
@@ -2,17 +2,22 @@ import os
2
2
  from pathlib import Path
3
3
 
4
4
  import yaml
5
- from pydantic import BaseModel, Field
5
+ from pydantic import BaseModel, ConfigDict, Field
6
6
 
7
7
  from qubx.core.interfaces import IStrategy
8
8
 
9
9
 
10
- class ConnectorConfig(BaseModel):
10
+ class StrictBaseModel(BaseModel):
11
+ """Base model with strict validation that forbids extra fields."""
12
+ model_config = ConfigDict(extra="forbid")
13
+
14
+
15
+ class ConnectorConfig(StrictBaseModel):
11
16
  connector: str
12
17
  params: dict = Field(default_factory=dict)
13
18
 
14
19
 
15
- class ExchangeConfig(BaseModel):
20
+ class ExchangeConfig(StrictBaseModel):
16
21
  connector: str
17
22
  universe: list[str]
18
23
  params: dict = Field(default_factory=dict)
@@ -20,22 +25,22 @@ class ExchangeConfig(BaseModel):
20
25
  account: ConnectorConfig | None = None
21
26
 
22
27
 
23
- class ReaderConfig(BaseModel):
28
+ class ReaderConfig(StrictBaseModel):
24
29
  reader: str
25
30
  args: dict = Field(default_factory=dict)
26
31
 
27
32
 
28
- class TypedReaderConfig(BaseModel):
33
+ class TypedReaderConfig(StrictBaseModel):
29
34
  data_type: list[str] | str
30
35
  readers: list[ReaderConfig]
31
36
 
32
37
 
33
- class RestorerConfig(BaseModel):
38
+ class RestorerConfig(StrictBaseModel):
34
39
  type: str
35
40
  parameters: dict = Field(default_factory=dict)
36
41
 
37
42
 
38
- class PrefetchConfig(BaseModel):
43
+ class PrefetchConfig(StrictBaseModel):
39
44
  enabled: bool = True
40
45
  prefetch_period: str = "1w"
41
46
  cache_size_mb: int = 1000
@@ -43,13 +48,13 @@ class PrefetchConfig(BaseModel):
43
48
  args: dict = Field(default_factory=dict)
44
49
 
45
50
 
46
- class WarmupConfig(BaseModel):
51
+ class WarmupConfig(StrictBaseModel):
47
52
  readers: list[TypedReaderConfig] = Field(default_factory=list)
48
53
  restorer: RestorerConfig | None = None
49
54
  prefetch: PrefetchConfig | None = None
50
55
 
51
56
 
52
- class LoggingConfig(BaseModel):
57
+ class LoggingConfig(StrictBaseModel):
53
58
  logger: str
54
59
  position_interval: str
55
60
  portfolio_interval: str
@@ -57,12 +62,12 @@ class LoggingConfig(BaseModel):
57
62
  heartbeat_interval: str = "1m"
58
63
 
59
64
 
60
- class ExporterConfig(BaseModel):
65
+ class ExporterConfig(StrictBaseModel):
61
66
  exporter: str
62
67
  parameters: dict = Field(default_factory=dict)
63
68
 
64
69
 
65
- class EmitterConfig(BaseModel):
70
+ class EmitterConfig(StrictBaseModel):
66
71
  """Configuration for a single metric emitter."""
67
72
 
68
73
  emitter: str
@@ -70,7 +75,7 @@ class EmitterConfig(BaseModel):
70
75
  tags: dict[str, str] = Field(default_factory=dict)
71
76
 
72
77
 
73
- class EmissionConfig(BaseModel):
78
+ class EmissionConfig(StrictBaseModel):
74
79
  """Configuration for metric emission."""
75
80
 
76
81
  stats_interval: str = "1m" # Default interval for emitting strategy stats
@@ -78,20 +83,21 @@ class EmissionConfig(BaseModel):
78
83
  emitters: list[EmitterConfig] = Field(default_factory=list)
79
84
 
80
85
 
81
- class NotifierConfig(BaseModel):
86
+ class NotifierConfig(StrictBaseModel):
82
87
  """Configuration for strategy lifecycle notifiers."""
83
88
 
84
89
  notifier: str
85
90
  parameters: dict = Field(default_factory=dict)
86
91
 
87
92
 
88
- class HealthConfig(BaseModel):
93
+ class HealthConfig(StrictBaseModel):
94
+ emit_health: bool = False
89
95
  emit_interval: str = "10s"
90
96
  queue_monitor_interval: str = "1s"
91
97
  buffer_size: int = 5000
92
98
 
93
99
 
94
- class LiveConfig(BaseModel):
100
+ class LiveConfig(StrictBaseModel):
95
101
  read_only: bool = False
96
102
  exchanges: dict[str, ExchangeConfig]
97
103
  logging: LoggingConfig
@@ -103,7 +109,7 @@ class LiveConfig(BaseModel):
103
109
  aux: list[ReaderConfig] | ReaderConfig | None = None
104
110
 
105
111
 
106
- class SimulationConfig(BaseModel):
112
+ class SimulationConfig(StrictBaseModel):
107
113
  capital: float
108
114
  instruments: list[str]
109
115
  start: str
@@ -121,7 +127,7 @@ class SimulationConfig(BaseModel):
121
127
  portfolio_log_freq: str | None = None
122
128
 
123
129
 
124
- class StrategyConfig(BaseModel):
130
+ class StrategyConfig(StrictBaseModel):
125
131
  name: str | None = None
126
132
  description: str | list[str] | None = None
127
133
  strategy: str | list[str] | type[IStrategy]
@@ -188,3 +194,100 @@ def load_strategy_config_from_yaml(path: Path | str, key: str | None = None) ->
188
194
  if key:
189
195
  config_dict = config_dict[key]
190
196
  return StrategyConfig(**config_dict)
197
+
198
+
199
+ class ValidationResult(StrictBaseModel):
200
+ """Result of configuration validation."""
201
+ valid: bool
202
+ errors: list[str] = Field(default_factory=list)
203
+ warnings: list[str] = Field(default_factory=list)
204
+
205
+
206
+ def validate_strategy_config(path: Path | str, check_imports: bool = True) -> ValidationResult:
207
+ """
208
+ Validates a strategy configuration file.
209
+
210
+ Args:
211
+ path: Path to the strategy configuration YAML file.
212
+ check_imports: Whether to verify strategy class can be imported (default: True).
213
+
214
+ Returns:
215
+ ValidationResult with validation status, errors, and warnings.
216
+ """
217
+ result = ValidationResult(valid=True)
218
+
219
+ # Check if file exists
220
+ path = Path(os.path.expanduser(path))
221
+ if not path.exists():
222
+ result.valid = False
223
+ result.errors.append(f"Configuration file not found: {path}")
224
+ return result
225
+
226
+ # Try to load and parse YAML
227
+ try:
228
+ config = load_strategy_config_from_yaml(path)
229
+ except yaml.YAMLError as e:
230
+ result.valid = False
231
+ result.errors.append(f"YAML parsing error: {e}")
232
+ return result
233
+ except Exception as e:
234
+ result.valid = False
235
+ result.errors.append(f"Configuration parsing error: {e}")
236
+ return result
237
+
238
+ # Validate strategy class can be imported
239
+ if check_imports:
240
+ if isinstance(config.strategy, str):
241
+ try:
242
+ from qubx.utils.misc import class_import
243
+ class_import(config.strategy)
244
+ except Exception as e:
245
+ result.valid = False
246
+ result.errors.append(f"Failed to import strategy '{config.strategy}': {e}")
247
+ elif isinstance(config.strategy, list):
248
+ for strat in config.strategy:
249
+ try:
250
+ from qubx.utils.misc import class_import
251
+ class_import(strat)
252
+ except Exception as e:
253
+ result.valid = False
254
+ result.errors.append(f"Failed to import strategy '{strat}': {e}")
255
+
256
+ # Validate live configuration if present
257
+ if config.live:
258
+ if not config.live.exchanges:
259
+ result.valid = False
260
+ result.errors.append("Live configuration requires at least one exchange")
261
+
262
+ for exchange_name, exchange_config in config.live.exchanges.items():
263
+ if not exchange_config.universe:
264
+ result.valid = False
265
+ result.errors.append(f"Exchange '{exchange_name}' has no symbols in universe")
266
+
267
+ if exchange_config.connector.lower() not in ["ccxt", "tardis"]:
268
+ result.warnings.append(f"Exchange '{exchange_name}' uses unknown connector: {exchange_config.connector}")
269
+
270
+ # Validate simulation configuration if present
271
+ if config.simulation:
272
+ if not config.simulation.instruments:
273
+ result.valid = False
274
+ result.errors.append("Simulation configuration requires at least one instrument")
275
+
276
+ if config.simulation.capital <= 0:
277
+ result.valid = False
278
+ result.errors.append("Simulation capital must be positive")
279
+
280
+ # Validate date format
281
+ try:
282
+ import pandas as pd
283
+ pd.Timestamp(config.simulation.start)
284
+ pd.Timestamp(config.simulation.stop)
285
+ except Exception as e:
286
+ result.valid = False
287
+ result.errors.append(f"Invalid simulation date format: {e}")
288
+
289
+ # Check that at least one mode (live or simulation) is configured
290
+ if not config.live and not config.simulation:
291
+ result.warnings.append("Configuration has neither 'live' nor 'simulation' section")
292
+
293
+ return result
@@ -304,10 +304,6 @@ def create_strategy_context(
304
304
  _chan = CtrlChannel("databus", sentinel=(None, None, None, None))
305
305
  _sched = BasicScheduler(_chan, lambda: _time.time().item())
306
306
 
307
- # Create time provider for metric emitters
308
- if _metric_emitter is not None:
309
- _metric_emitter.set_time_provider(_time)
310
-
311
307
  # Create health metrics monitor with emitter
312
308
  _health_monitor = BaseHealthMonitor(
313
309
  _time, emitter=_metric_emitter, channel=_chan, **config.live.health.model_dump()
@@ -390,6 +386,10 @@ def create_strategy_context(
390
386
  restored_state=restored_state,
391
387
  )
392
388
 
389
+ # Set context for metric emitters to enable is_live tag and time access
390
+ if _metric_emitter is not None:
391
+ _metric_emitter.set_context(ctx)
392
+
393
393
  return ctx
394
394
 
395
395
 
@@ -709,9 +709,9 @@ def _run_warmup(
709
709
  finally:
710
710
  # Restore the live time provider
711
711
  simulated_formatter.time_provider = _live_time_provider
712
- # Set back the time provider for metric emitters to use live time provider
712
+ # Set back the context for metric emitters to use live context
713
713
  if ctx.emitter is not None:
714
- ctx.emitter.set_time_provider(_live_time_provider)
714
+ ctx.emitter.set_context(ctx)
715
715
  ctx._strategy_state.is_warmup_in_progress = False
716
716
  ctx.initializer.simulation = False
717
717
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Qubx
3
- Version: 0.6.85
3
+ Version: 0.6.87
4
4
  Summary: Qubx - Quantitative Trading Framework
5
5
  License-File: LICENSE
6
6
  Author: Dmitry Marienko
@@ -4,17 +4,17 @@ qubx/backtester/__init__.py,sha256=OhXhLmj2x6sp6k16wm5IPATvv-E2qRZVIcvttxqPgcg,1
4
4
  qubx/backtester/account.py,sha256=0yvE06icSeK2ymovvaKkuftY8Ou3Z7Y2JrDa6VtkINw,3048
5
5
  qubx/backtester/broker.py,sha256=lJ6PpqE2L0vKcgw2ZP7xahne4eZguJFYWXHiWP6jmXM,2949
6
6
  qubx/backtester/data.py,sha256=eDS-fe44m6dKy3hp2P7Gll2HRZRpsf_EG-K52Z3zsx8,8057
7
- qubx/backtester/management.py,sha256=uyGgffhGSXKWAAxQhyuq-YzHmJjQb9AwJP-AeIQpkeY,21070
7
+ qubx/backtester/management.py,sha256=cgeO7cIyd5J9r1XYGWC9XV2oVHtgAMpQZ6rhxFYl0J4,21147
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=cUHFfHrxa0YO-9TYkaIGqQfaTc9ocq-FppE3LPxCzQM,25438
10
+ qubx/backtester/runner.py,sha256=D1zCw6_qscaZZkSAm1JY_I8XsZARz4TyYn48hTzdDUE,25420
11
11
  qubx/backtester/sentinels.py,sha256=wuRfPHpepxXwTBxtbLxcGfK2BpnUn_j-Gbg103MQL38,792
12
12
  qubx/backtester/simulated_data.py,sha256=QuVrs69-IeQLnkcax9N5VMtHjbxEId9cmUY_b9eRSYg,19678
13
13
  qubx/backtester/simulated_exchange.py,sha256=6ZsxkfTkp5CfFU_Fzjm7LwsQk-GTA3Nm00tDUD-Ja8I,8164
14
14
  qubx/backtester/simulator.py,sha256=iAbz_XJhoXFRSgYxLgtLR49RN7cZwA3W1q6y40e-PF4,13364
15
15
  qubx/backtester/utils.py,sha256=SZRHL1eQxZLArewqcyVL4QvPVXuFUd-XKf6IFhBgW4w,37951
16
16
  qubx/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
- qubx/cli/commands.py,sha256=z1Qs6jLbrfINk7D6V7n6ruPNLoyq3T-sY66Y-F8LmBE,11714
17
+ qubx/cli/commands.py,sha256=riVAnPSCChPis0LMx7cCtoKK4JPh1uDt2TOElyni1sM,13395
18
18
  qubx/cli/deploy.py,sha256=pQ9FPOsywDyy8jOjLfrgYTTkKQ-MCixCzbgsG68Q3_0,8319
19
19
  qubx/cli/misc.py,sha256=tP28QxLEzuP8R2xnt8g3JTs9Z7aYy4iVWY4g3VzKTsQ,14777
20
20
  qubx/cli/release.py,sha256=Kz5aykF9FlAeUgXF59_Mu3vRFxa_qF9dq0ySqLlKKqo,41362
@@ -36,20 +36,20 @@ qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py,sha256=Tq-OR06JBU2yADUQc7m80
36
36
  qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py,sha256=zrnA6GJiNddoM5JF-SlFFO4FpITDO5rGaU9ipshUvAY,1603
37
37
  qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py,sha256=j6OB7j_yKRgitQAeJX3jkTrJ7NVnbTKtoumMiH_9u4I,166
38
38
  qubx/connectors/ccxt/exchanges/hyperliquid/broker.py,sha256=yoht8KsiMS3F6rkyfwQ3prBn-OgnY6tCDwYJQbsq1t0,3130
39
- qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py,sha256=w2uidBYpIUWoZsAeXiITdc77EIGN4cE3bXsGAb4JpaE,20353
39
+ qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py,sha256=nHeMCBfIBdcWYl8s9vqyr9fxVmnzEz0uEwxQskLN6UM,20353
40
40
  qubx/connectors/ccxt/exchanges/kraken/kraken.py,sha256=ntxf41aPY0JqxT5jn-979fgQih2TvMbr_p9fvCJB9QE,414
41
41
  qubx/connectors/ccxt/factory.py,sha256=hX5WODwwo49J3YjGulpDyq1SKq9VvWH3otzFw6avS5I,6259
42
42
  qubx/connectors/ccxt/handlers/__init__.py,sha256=Cuo2RsHiijhBd8YIkLey9JhHkBGWRtPoeu_BEg_jlDE,879
43
43
  qubx/connectors/ccxt/handlers/base.py,sha256=1QS9uhFsisWvGwDmoAc83kLcSd1Lf3hSlnrWwKoHuAk,3334
44
44
  qubx/connectors/ccxt/handlers/factory.py,sha256=SlMAOmaUy1dQ-q5ZzrjODj_XMcLr1BORGGZ7klLNndk,4250
45
- qubx/connectors/ccxt/handlers/funding_rate.py,sha256=OLlocxRBEMxQ7LAtxZgQcdn3LIZwAqxZ7Q69TUYNyT4,8918
45
+ qubx/connectors/ccxt/handlers/funding_rate.py,sha256=w7uRasxE9084FT6lekXoC9G3QY9ucgn3Garfq4161XY,8998
46
46
  qubx/connectors/ccxt/handlers/liquidation.py,sha256=0QbLeZwpdEgd30PwQoWXUBKG25rrNQ0OgPz8fPLRA6U,3915
47
47
  qubx/connectors/ccxt/handlers/ohlc.py,sha256=668mHEuLb-sT-Rk0miNsvKTETfoo0XK9aAAqNa4uTCY,17706
48
48
  qubx/connectors/ccxt/handlers/open_interest.py,sha256=K3gPI51zUtxvED3V-sfFmK1Lsa-vx7k-Q-UE_eLsoRM,9957
49
49
  qubx/connectors/ccxt/handlers/orderbook.py,sha256=67cJt1aW778lMch8abkzvkhRKKnfHOqJ6IDufx-NoZk,9129
50
50
  qubx/connectors/ccxt/handlers/quote.py,sha256=JwQ8mXMpFMdFEpQTx3x_Xaj6VHZanC6_JI6tB9l_yDw,4464
51
51
  qubx/connectors/ccxt/handlers/trade.py,sha256=doBxWHvRQIhRbr5PqzQnoVMBarjWlezk75DPlu__JVg,8841
52
- qubx/connectors/ccxt/reader.py,sha256=uUG1I_ejzTf0f4bCAHpLhBzTUqtNX-JvJGFA4bi7-WU,26602
52
+ qubx/connectors/ccxt/reader.py,sha256=T1PXe2zqkOsyNEoE5-d9NWwZEBTPzbpTR2R5Fc6K9zg,26613
53
53
  qubx/connectors/ccxt/subscription_config.py,sha256=jbMZ_9US3nvrp6LCVmMXLQnAjXH0xIltzUSPqXJZvgs,3865
54
54
  qubx/connectors/ccxt/subscription_manager.py,sha256=9ZfA6bR6YwtlZqVs6yymKPvYSvy5x3yBuHA_LDbiKgc,13285
55
55
  qubx/connectors/ccxt/subscription_orchestrator.py,sha256=CbZMTRhmgcJZd8cofQbyBDI__N2Lbo1loYfh9_-EkFA,16512
@@ -66,10 +66,10 @@ qubx/core/errors.py,sha256=LENtlgmVzxxUFNCsuy4PwyHYhkZkxuZQ2BPif8jaGmw,1411
66
66
  qubx/core/exceptions.py,sha256=11wQC3nnNLsl80zBqbE6xiKCqm31kctqo6W_gdnZkg8,581
67
67
  qubx/core/helpers.py,sha256=xFcgXtNGvaz8SvLeHToePMx0tx85eX3H4ymxjz66ekg,22129
68
68
  qubx/core/initializer.py,sha256=VVti9UJDJCg0125Mm09S6Tt1foHK4XOBL0mXgDmvXes,7724
69
- qubx/core/interfaces.py,sha256=AwSS92BbfUXw92TLvB75Wn5bxYByHAAXie2GAC70vUQ,67824
69
+ qubx/core/interfaces.py,sha256=cf1-_cNGIucXI8be3BLXwBXBBGVB_88x33eh1hLsZJc,67906
70
70
  qubx/core/loggers.py,sha256=eYijsR02S5u1Hv21vjIk_dOUwOMv0fiBDYwEmFhAoWk,14344
71
71
  qubx/core/lookups.py,sha256=Bed30kPZvbTGjZ8exojhIMOIVfB46j6741yF3fXGTiM,18313
72
- qubx/core/metrics.py,sha256=o3Bd25G2GwqxVlIpryScCrQISCYjsVWiIWAUUsW2siQ,79274
72
+ qubx/core/metrics.py,sha256=j8fXGs946reDLugywxTuWPB8WhZbsoZpwVVDUFePcJE,81480
73
73
  qubx/core/mixins/__init__.py,sha256=AMCLvfNuIb1kkQl3bhCj9jIOEl2eKcVPJeyLgrkB-rk,329
74
74
  qubx/core/mixins/market.py,sha256=w9OEDfy0r9xnb4KdKA-PuFsCQugNo4pMLQivGFeyqGw,6356
75
75
  qubx/core/mixins/processing.py,sha256=UqKwOzkDXvmcjvffrsR8vugq_XacwW9rcGM0TcBd9RM,44485
@@ -77,12 +77,12 @@ qubx/core/mixins/subscription.py,sha256=2nUikNNPsMExS9yO38kNt7jk1vKE-RPm0b3h1bU6
77
77
  qubx/core/mixins/trading.py,sha256=7KwxHiPWkXGnkHS4VLaxOZ7BHULkvvqPS8ooAG1NTxM,9477
78
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=OvwHlIEZOnDhfIH_uRteowsd3l6OMYM6cywPPS4vSkQ,1027944
80
+ qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=MddcS0urAPBzGMDy7UmReGkfyabd8zBjPqmnGO87quk,1027944
81
81
  qubx/core/series.pxd,sha256=4WpEexOBwZdKvqI81yR7wCnQh2rKgVZp9Y0ejr8B3E4,4841
82
82
  qubx/core/series.pyi,sha256=30F48-oMp-XuySh5pyuoZcV20R4yODRpYBgLv7yxhjY,5534
83
83
  qubx/core/series.pyx,sha256=9d3XjPAyI6qYLXuKXqZ3HrxBir1CRVste8GpGvgqYL4,52876
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=XhIYTnpumKGrmjGzWNiU9_IJMENmqJmJZhWVmt4YS58,86568
85
+ qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=xfIqHcMOQGDGkPXRl4yQwFOFLKrkVk0jckn1p-j6DHs,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
@@ -93,19 +93,20 @@ qubx/data/readers.py,sha256=ehTyA54f8P4ddqfsohXGUIZtvTd5TniNfod2AoW-cP0,76540
93
93
  qubx/data/registry.py,sha256=SqCZ9Q0BZiHW2gC9yRuiVRV0lejyJAHI-694Yl_Cfdo,3892
94
94
  qubx/data/tardis.py,sha256=O-zglpusmO6vCY3arSOgH6KUbkfPajSAIQfMKlVmh_E,33878
95
95
  qubx/emitters/__init__.py,sha256=MPs7ZRZZnURljusiuvlO5g8M4H1UjEfg5fkyKeJmIBI,791
96
- qubx/emitters/base.py,sha256=lxNmP81pXuRo0LKjjxkGqn0LCYjWDiqJ94dQdosGugg,8744
97
- qubx/emitters/composite.py,sha256=JkFch4Tp5q6CaLU2nAmeZnRiVPGkFhGNvzhT255yJfI,3411
98
- qubx/emitters/csv.py,sha256=S-oQ84rCgP-bb2_q-FWcegACg_Ej_Ik3tXE6aJBlqOk,4963
99
- qubx/emitters/indicator.py,sha256=NlhXJAZCboUDz7M7MOjfiR-ASM_L5qv0KgPJE-ekQCY,8206
100
- qubx/emitters/inmemory.py,sha256=AsFpAGGTWQsv42H5-3tDeZ3XP9b5Ye7lFHis53qcdjs,8862
101
- qubx/emitters/prometheus.py,sha256=lZJ_Hl-AlkeWJmktxhAiEMiTIc8dTQvBpf3Ih5Fy6pE,10516
102
- qubx/emitters/questdb.py,sha256=hGneKVEnkV82t7c9G3_uVEgAoN302nXBSjjCWHKnAv0,11532
96
+ qubx/emitters/base.py,sha256=uEgdWwCkDdzVyFWlDmlw7INv2BSzDp9WC2ccuaKp-rA,9067
97
+ qubx/emitters/composite.py,sha256=BXWPtR-e9stWCYdQYO3SK_LWmyt071r-zSIgM44p__s,3847
98
+ qubx/emitters/csv.py,sha256=vVAfeW40UW4KiTum6x1BHYOO2WRFwX0MbZyKBD7EAwM,4986
99
+ qubx/emitters/indicator.py,sha256=3sZbrlXmIOWQUeTYQAu92AS-2gfa9z5f_MdiM3KIBVY,8230
100
+ qubx/emitters/inmemory.py,sha256=mDPlncqd7Bz4rrr4gJezYaMd9tPUaurfqRg4STCdNXc,8891
101
+ qubx/emitters/prometheus.py,sha256=HmQdhL6Nu-UzxAkahZRjFMoDqxhfg8mlEVpH_ozYc2Q,10521
102
+ qubx/emitters/questdb.py,sha256=o_sQQZIG0mSjzM2PtSk1ki-IDSD-HIwaL-Fw326TTy8,11810
103
103
  qubx/exporters/__init__.py,sha256=7HeYHCZfKAaBVAByx9wE8DyGv6C55oeED9uUphcyjuc,360
104
104
  qubx/exporters/composite.py,sha256=c45XcMC0dsIDwOyOxxCuiyYQjUNhqPjptAulbaSqttU,2973
105
- qubx/exporters/formatters/__init__.py,sha256=La9rMsl3wyplza0xVyAFrUwhFyrGDIMJWmOB_boJyIg,488
105
+ qubx/exporters/formatters/__init__.py,sha256=nyPFrsRJczffszAV2gXE_23qwRPOkNJo72PhI9LiI5g,616
106
106
  qubx/exporters/formatters/base.py,sha256=j381c-JgjUnUHJF7k1J1MPeHB0sFDC9xNcFt2jZNhNY,5671
107
107
  qubx/exporters/formatters/incremental.py,sha256=V8kbxKDqjQm82wR8wDjHs2bR5WsHqSDFn_O5mI3yBxg,5260
108
108
  qubx/exporters/formatters/slack.py,sha256=MPjbEFh7PQufPdkg_Fwiu2tVw5zYJa977tCemoI790Y,7017
109
+ qubx/exporters/formatters/target_position.py,sha256=UxeoVblYlYbCC_55jkq7Z0JENnFO8DIa6lgSWLM55Hw,2985
109
110
  qubx/exporters/redis_streams.py,sha256=ywbZrrqJDbMmV4QIW_XHBFh8NZu8gbOb47P3WH6UXIk,9257
110
111
  qubx/exporters/slack.py,sha256=wnVZRwWOKq9lMQyW0MWh_6gkW1id1TUanfOKy-_clwI,7723
111
112
  qubx/features/__init__.py,sha256=ZFCX7K5bDAH7yTsG-mf8zibW8UW8GCneEagL1_p8kDQ,385
@@ -116,7 +117,7 @@ qubx/features/trades.py,sha256=fGr6pVKCzK0mEsrkRomeU-kNO6WzNvEf-J5Iuyte3PY,3603
116
117
  qubx/features/utils.py,sha256=5wMlfH4x1dUh00dxvtnHhSiHeRaiod4VMTcmgm-o_wA,264
117
118
  qubx/gathering/simplest.py,sha256=7SHTwK5MojtgjDR5pNObGZCD1EFK15ewG58oGonCC_8,4409
118
119
  qubx/health/__init__.py,sha256=ThJTgf-CPD5tMU_emqANpnE6oXfUmzyyugfbDfzeVB0,111
119
- qubx/health/base.py,sha256=3EKoZmtRVpDynM2Bm8WIzMNOSt3Y0J3rg-7yOzeqTaY,27737
120
+ qubx/health/base.py,sha256=RIm2nc_FPBBJ9Ji3w_mlIwISjmGR8K94iqRjgVvkGSY,27941
120
121
  qubx/loggers/__init__.py,sha256=nA7nLQKkR9hIJCYQyZxikm--xsB6DaTE5itKypEPBKA,443
121
122
  qubx/loggers/csv.py,sha256=FCzGSPz1OvqshoRD-pHwfpndwsZbL1S4i7ou57bNAuE,4390
122
123
  qubx/loggers/factory.py,sha256=pDwLuFPPpoCCTiVoDrzvcAsyPFm6sS0e4Zi3j5UEhqk,1791
@@ -156,7 +157,7 @@ qubx/restorers/signal.py,sha256=5nK5ji8AucyWrFBK9uW619YCI_vPRGFnuDu8JnG3B_Y,1451
156
157
  qubx/restorers/state.py,sha256=I1VIN0ZcOjigc3WMHIYTNJeAAbN9YB21MDcMl04ZWmY,8018
157
158
  qubx/restorers/utils.py,sha256=We2gfqwQKWziUYhuUnjb-xo-5tSlbuHWpPQn0CEMTn0,1155
158
159
  qubx/ta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
159
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=4SOZrbMeBBi7ep88enwT7hZh-KwHlyL_T4muHfF41bw,804392
160
+ qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=B5_SZK5aRn4v2aG6QJz3aOTnLDkkVN6UvsHakfN9uhA,804392
160
161
  qubx/ta/indicators.pxd,sha256=r9mYcpDFxn3RW5lVJ497Fiq2-eqD4k7VX0-Q0xcftkk,4913
161
162
  qubx/ta/indicators.pyi,sha256=T87VwJrBJK8EuqtoW1Dhjri05scH_Y6BXN9kex6G7mQ,2881
162
163
  qubx/ta/indicators.pyx,sha256=jJMZ7D2kUspwiZITBnFuNPNvzqUm26EtEfSzBWSrLvQ,39286
@@ -206,13 +207,13 @@ qubx/utils/questdb.py,sha256=bxlWiCyYf8IspsvXrs58tn5iXYBUtv6ojeYwOj8EXI0,5269
206
207
  qubx/utils/runner/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
207
208
  qubx/utils/runner/_jupyter_runner.pyt,sha256=DHXhXkjHe8-HkOa4g5EkSb3qbz64TLyM3-c__cQDPjk,9973
208
209
  qubx/utils/runner/accounts.py,sha256=mpiv6oxr5z97zWt7STYyARMhWQIpc_XFKungb_pX38U,3270
209
- qubx/utils/runner/configs.py,sha256=ed0vuK9YYfvTpE1E0VaKCuwyP1XDf985JV1LDr6W-JI,5409
210
+ qubx/utils/runner/configs.py,sha256=vAhVtkB95q9JVIMOvXEAp1pOQoI33rz40Oalt25J5kM,9353
210
211
  qubx/utils/runner/factory.py,sha256=hmtUDYNFQwVQffHEfxgrlmKwOGLcFQ6uJIH_ZLscpIY,16347
211
- qubx/utils/runner/runner.py,sha256=xc575tmIOWuI1UN1YTdBsW5iA3WdhaQzR0XawV1UcPo,34337
212
+ qubx/utils/runner/runner.py,sha256=q3b80zD4za6dFSOA6mUCuSQXe0DKS6LwGBMxO1aL3aw,34324
212
213
  qubx/utils/time.py,sha256=xOWl_F6dOLFCmbB4xccLIx5yVt5HOH-I8ZcuowXjtBQ,11797
213
214
  qubx/utils/version.py,sha256=e52fIHyxzCiIuH7svCF6pkHuDlqL64rklqz-2XjWons,5309
214
- qubx-0.6.85.dist-info/METADATA,sha256=ZOHrVXgrzG-6FeBQhqdnU41BZI1KUaCvdDAERuUw4EY,5909
215
- qubx-0.6.85.dist-info/WHEEL,sha256=RA6gLSyyVpI0R7d3ofBrM1iY5kDUsPwh15AF0XpvgQo,110
216
- qubx-0.6.85.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
217
- qubx-0.6.85.dist-info/licenses/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
218
- qubx-0.6.85.dist-info/RECORD,,
215
+ qubx-0.6.87.dist-info/METADATA,sha256=jcEgEqK55_KQcapsfhYAYhiWmRcSnwSaOSxeyqiFf7w,5909
216
+ qubx-0.6.87.dist-info/WHEEL,sha256=RA6gLSyyVpI0R7d3ofBrM1iY5kDUsPwh15AF0XpvgQo,110
217
+ qubx-0.6.87.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
218
+ qubx-0.6.87.dist-info/licenses/LICENSE,sha256=qwMHOSJ2TD0nx6VUJvFhu1ynJdBfNozRMt6tnSul-Ts,35140
219
+ qubx-0.6.87.dist-info/RECORD,,
File without changes