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.
- qubx/backtester/management.py +3 -2
- qubx/backtester/runner.py +1 -1
- qubx/cli/commands.py +46 -1
- qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +1 -1
- qubx/connectors/ccxt/handlers/funding_rate.py +3 -3
- qubx/connectors/ccxt/reader.py +3 -2
- qubx/core/interfaces.py +7 -6
- qubx/core/metrics.py +74 -14
- qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/emitters/base.py +23 -14
- qubx/emitters/composite.py +13 -0
- qubx/emitters/csv.py +2 -1
- qubx/emitters/indicator.py +4 -2
- qubx/emitters/inmemory.py +5 -4
- qubx/emitters/prometheus.py +2 -2
- qubx/emitters/questdb.py +16 -10
- qubx/exporters/formatters/__init__.py +8 -1
- qubx/exporters/formatters/target_position.py +78 -0
- qubx/health/base.py +7 -10
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/utils/runner/configs.py +120 -17
- qubx/utils/runner/runner.py +6 -6
- {qubx-0.6.85.dist-info → qubx-0.6.87.dist-info}/METADATA +1 -1
- {qubx-0.6.85.dist-info → qubx-0.6.87.dist-info}/RECORD +28 -27
- {qubx-0.6.85.dist-info → qubx-0.6.87.dist-info}/WHEEL +0 -0
- {qubx-0.6.85.dist-info → qubx-0.6.87.dist-info}/entry_points.txt +0 -0
- {qubx-0.6.85.dist-info → qubx-0.6.87.dist-info}/licenses/LICENSE +0 -0
qubx/backtester/management.py
CHANGED
|
@@ -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",
|
|
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",
|
|
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
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
|
|
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 =
|
|
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,
|
qubx/connectors/ccxt/reader.py
CHANGED
|
@@ -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"
|
|
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
|
-
|
|
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,
|
|
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
|
|
2095
|
+
def set_context(self, context: "IStrategyContext") -> None:
|
|
2096
2096
|
"""
|
|
2097
|
-
Set the
|
|
2097
|
+
Set the strategy context for the metric emitter.
|
|
2098
2098
|
|
|
2099
|
-
This method is used to set the
|
|
2100
|
-
|
|
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
|
-
|
|
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["
|
|
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
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
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(
|
|
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,
|
|
Binary file
|
|
Binary file
|
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
|
|
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,
|
|
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.
|
|
52
|
+
self._context = None
|
|
53
53
|
|
|
54
|
-
def _merge_tags(self, tags: dict[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,
|
|
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,
|
|
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,
|
|
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.
|
|
106
|
-
timestamp = self.
|
|
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
|
|
115
|
+
def set_context(self, context: IStrategyContext) -> None:
|
|
111
116
|
"""
|
|
112
|
-
Set the
|
|
117
|
+
Set the strategy context for the metric emitter.
|
|
113
118
|
|
|
114
119
|
Args:
|
|
115
|
-
|
|
120
|
+
context: The strategy context to use
|
|
116
121
|
"""
|
|
117
|
-
self.
|
|
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()
|
qubx/emitters/composite.py
CHANGED
|
@@ -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,
|
|
31
|
+
tags: dict[str, Any] | None = None,
|
|
31
32
|
):
|
|
32
33
|
"""
|
|
33
34
|
Initialize the CSV Metric Emitter.
|
qubx/emitters/indicator.py
CHANGED
|
@@ -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,
|
|
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,
|
|
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
|
|
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,
|
|
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
|
-
|
|
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
|
qubx/emitters/prometheus.py
CHANGED
|
@@ -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,
|
|
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,
|
|
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 =
|
|
147
|
-
|
|
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__ = [
|
|
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
|
|
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()
|
|
Binary file
|
qubx/utils/runner/configs.py
CHANGED
|
@@ -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
|
|
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(
|
|
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(
|
|
28
|
+
class ReaderConfig(StrictBaseModel):
|
|
24
29
|
reader: str
|
|
25
30
|
args: dict = Field(default_factory=dict)
|
|
26
31
|
|
|
27
32
|
|
|
28
|
-
class TypedReaderConfig(
|
|
33
|
+
class TypedReaderConfig(StrictBaseModel):
|
|
29
34
|
data_type: list[str] | str
|
|
30
35
|
readers: list[ReaderConfig]
|
|
31
36
|
|
|
32
37
|
|
|
33
|
-
class RestorerConfig(
|
|
38
|
+
class RestorerConfig(StrictBaseModel):
|
|
34
39
|
type: str
|
|
35
40
|
parameters: dict = Field(default_factory=dict)
|
|
36
41
|
|
|
37
42
|
|
|
38
|
-
class PrefetchConfig(
|
|
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(
|
|
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(
|
|
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(
|
|
65
|
+
class ExporterConfig(StrictBaseModel):
|
|
61
66
|
exporter: str
|
|
62
67
|
parameters: dict = Field(default_factory=dict)
|
|
63
68
|
|
|
64
69
|
|
|
65
|
-
class EmitterConfig(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
qubx/utils/runner/runner.py
CHANGED
|
@@ -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
|
|
712
|
+
# Set back the context for metric emitters to use live context
|
|
713
713
|
if ctx.emitter is not None:
|
|
714
|
-
ctx.emitter.
|
|
714
|
+
ctx.emitter.set_context(ctx)
|
|
715
715
|
ctx._strategy_state.is_warmup_in_progress = False
|
|
716
716
|
ctx.initializer.simulation = False
|
|
717
717
|
|
|
@@ -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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
97
|
-
qubx/emitters/composite.py,sha256=
|
|
98
|
-
qubx/emitters/csv.py,sha256=
|
|
99
|
-
qubx/emitters/indicator.py,sha256=
|
|
100
|
-
qubx/emitters/inmemory.py,sha256=
|
|
101
|
-
qubx/emitters/prometheus.py,sha256=
|
|
102
|
-
qubx/emitters/questdb.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
215
|
-
qubx-0.6.
|
|
216
|
-
qubx-0.6.
|
|
217
|
-
qubx-0.6.
|
|
218
|
-
qubx-0.6.
|
|
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
|
|
File without changes
|
|
File without changes
|