Qubx 0.6.53__tar.gz → 0.6.56__tar.gz
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-0.6.53 → qubx-0.6.56}/PKG-INFO +1 -1
- {qubx-0.6.53 → qubx-0.6.56}/pyproject.toml +1 -1
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/backtester/management.py +207 -76
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/backtester/runner.py +1 -1
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/basics.py +111 -5
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/helpers.py +18 -7
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/lookups.py +226 -304
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/mixins/processing.py +76 -17
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/data/composite.py +13 -1
- qubx-0.6.56/src/qubx/resources/crypto-fees.ini +98 -0
- qubx-0.6.56/src/qubx/resources/instruments/symbols-binance-spot.json +1 -0
- qubx-0.6.56/src/qubx/resources/instruments/symbols-binance.cm-future.json +1 -0
- qubx-0.6.56/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +1 -0
- qubx-0.6.56/src/qubx/resources/instruments/symbols-binance.um-future.json +1 -0
- qubx-0.6.56/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +1 -0
- qubx-0.6.56/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +1 -0
- qubx-0.6.56/src/qubx/resources/instruments/symbols-hyperliquid-spot.json +1 -0
- qubx-0.6.56/src/qubx/resources/instruments/symbols-hyperliquid.f-perpetual.json +1 -0
- qubx-0.6.56/src/qubx/resources/instruments/symbols-kraken-spot.json +1 -0
- qubx-0.6.56/src/qubx/resources/instruments/symbols-kraken.f-future.json +1 -0
- qubx-0.6.56/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +1 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/restorers/factory.py +3 -3
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/marketdata/ccxt.py +51 -6
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/marketdata/dukas.py +1 -1
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/runner/runner.py +1 -1
- qubx-0.6.53/src/qubx/resources/instruments/symbols-binance.cm.json +0 -1
- qubx-0.6.53/src/qubx/resources/instruments/symbols-binance.json +0 -1
- qubx-0.6.53/src/qubx/resources/instruments/symbols-binance.um.json +0 -1
- qubx-0.6.53/src/qubx/resources/instruments/symbols-bitfinex.f.json +0 -1
- qubx-0.6.53/src/qubx/resources/instruments/symbols-bitfinex.json +0 -1
- qubx-0.6.53/src/qubx/resources/instruments/symbols-kraken.f.json +0 -1
- qubx-0.6.53/src/qubx/resources/instruments/symbols-kraken.json +0 -1
- {qubx-0.6.53 → qubx-0.6.56}/LICENSE +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/README.md +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/build.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/__init__.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/_nb_magic.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/backtester/__init__.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/backtester/account.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/backtester/broker.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/backtester/data.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/backtester/ome.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/backtester/optimization.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/backtester/simulated_data.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/backtester/simulated_exchange.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/backtester/simulator.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/backtester/utils.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/cli/__init__.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/cli/commands.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/cli/deploy.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/cli/misc.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/cli/release.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/cli/tui.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/account.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/broker.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/data.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/factory.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/reader.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/utils.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/tardis/data.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/tardis/utils.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/__init__.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/account.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/context.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/deque.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/errors.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/exceptions.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/initializer.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/interfaces.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/loggers.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/metrics.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/mixins/market.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/mixins/subscription.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/mixins/trading.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/mixins/universe.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/series.pxd +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/series.pyi +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/series.pyx +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/utils.pyi +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/utils.pyx +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/data/__init__.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/data/helpers.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/data/hft.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/data/readers.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/data/registry.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/data/tardis.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/emitters/__init__.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/emitters/base.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/emitters/composite.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/emitters/csv.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/emitters/prometheus.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/emitters/questdb.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/exporters/__init__.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/exporters/composite.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/exporters/formatters/__init__.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/exporters/formatters/base.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/exporters/formatters/incremental.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/exporters/formatters/slack.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/exporters/redis_streams.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/exporters/slack.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/features/__init__.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/features/core.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/features/orderbook.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/features/price.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/features/trades.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/features/utils.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/gathering/simplest.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/health/__init__.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/health/base.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/loggers/__init__.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/loggers/csv.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/loggers/factory.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/loggers/inmemory.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/loggers/mongo.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/math/__init__.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/math/stats.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/notifications/__init__.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/notifications/composite.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/notifications/slack.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/notifications/throttler.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/pandaz/ta.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/pandaz/utils.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/resources/_build.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/restarts/__init__.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/restarts/state_resolvers.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/restarts/time_finders.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/restorers/__init__.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/restorers/balance.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/restorers/interfaces.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/restorers/position.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/restorers/signal.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/restorers/state.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/restorers/utils.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/ta/__init__.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/trackers/__init__.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/trackers/advanced.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/trackers/composite.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/trackers/riskctrl.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/trackers/sizers.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/__init__.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/collections.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/misc.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/ntp.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/orderbook.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/questdb.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/runner/configs.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/runner/factory.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/time.py +0 -0
- {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/version.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "Qubx"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.56"
|
|
8
8
|
description = "Qubx - Quantitative Trading Framework"
|
|
9
9
|
authors = [ "Dmitry Marienko <dmitry.marienko@xlydian.com>", "Yuriy Arabskyy <yuriy.arabskyy@xlydian.com>",]
|
|
10
10
|
readme = "README.md"
|
|
@@ -3,6 +3,7 @@ import zipfile
|
|
|
3
3
|
from collections import defaultdict
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
+
import numpy as np
|
|
6
7
|
import pandas as pd
|
|
7
8
|
import yaml
|
|
8
9
|
|
|
@@ -230,6 +231,25 @@ class BacktestsResultsManager:
|
|
|
230
231
|
|
|
231
232
|
yield info.get("idx", -1)
|
|
232
233
|
|
|
234
|
+
def list_variations(self, regex: str = "", detailed=True, sort_by: str | None = "sharpe", ascending=False):
|
|
235
|
+
"""
|
|
236
|
+
List only variations of a backtest result.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
- regex (str, optional): Regular expression pattern to filter results by strategy name or class. Defaults to "".
|
|
240
|
+
- sort_by (str, optional): The criterion to sort the results by. Defaults to "sharpe".
|
|
241
|
+
- ascending (bool, optional): Whether to sort the results in ascending order. Defaults to False.
|
|
242
|
+
- detailed (bool, optional): Whether to show each variation run. Defaults to True.
|
|
243
|
+
"""
|
|
244
|
+
return self.list(
|
|
245
|
+
regex=regex,
|
|
246
|
+
sort_by=sort_by,
|
|
247
|
+
ascending=ascending,
|
|
248
|
+
show_variations=True,
|
|
249
|
+
show_simulations=False,
|
|
250
|
+
show_each_variation_run=detailed,
|
|
251
|
+
)
|
|
252
|
+
|
|
233
253
|
def list(
|
|
234
254
|
self,
|
|
235
255
|
regex: str = "",
|
|
@@ -239,7 +259,9 @@ class BacktestsResultsManager:
|
|
|
239
259
|
pretty_print=False,
|
|
240
260
|
sort_by: str | None = "sharpe",
|
|
241
261
|
ascending=False,
|
|
262
|
+
show_simulations=True,
|
|
242
263
|
show_variations=True,
|
|
264
|
+
show_each_variation_run=True,
|
|
243
265
|
):
|
|
244
266
|
"""List backtesting results with optional filtering and formatting.
|
|
245
267
|
|
|
@@ -248,87 +270,93 @@ class BacktestsResultsManager:
|
|
|
248
270
|
- with_metrics (bool, optional): Whether to include performance metrics in output. Defaults to True.
|
|
249
271
|
- params (bool, optional): Whether to display strategy parameters. Defaults to False.
|
|
250
272
|
- as_table (bool, optional): Return results as a pandas DataFrame instead of printing. Defaults to False.
|
|
273
|
+
- sort_by (str, optional): The criterion to sort the results by. Defaults to "sharpe".
|
|
274
|
+
- ascending (bool, optional): Whether to sort the results in ascending order. Defaults to False.
|
|
275
|
+
- show_simulations (bool, optional): Whether to show simulation results. Defaults to True.
|
|
276
|
+
- show_variations (bool, optional): Whether to show variation results. Defaults to True.
|
|
277
|
+
- show_each_variation_run (bool, optional): Whether to show each variation run. Defaults to True.
|
|
251
278
|
|
|
252
279
|
Returns:
|
|
253
280
|
- Optional[pd.DataFrame]: If as_table=True, returns a DataFrame containing the results sorted by creation time.
|
|
254
281
|
- Otherwise prints formatted results to console.
|
|
255
282
|
"""
|
|
256
283
|
_t_rep = []
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
284
|
+
if show_simulations:
|
|
285
|
+
for n in sorted(self.results.keys()):
|
|
286
|
+
info = self.results[n]
|
|
287
|
+
s_cls = info.get("strategy_class", "").split(".")[-1]
|
|
260
288
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
289
|
+
if regex:
|
|
290
|
+
if not re.match(regex, n, re.IGNORECASE):
|
|
291
|
+
# if not re.match(regex, s_cls, re.IGNORECASE):
|
|
292
|
+
continue
|
|
265
293
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
if not as_table:
|
|
299
|
-
print(_s)
|
|
294
|
+
name = info.get("name", "")
|
|
295
|
+
smbs = ", ".join(info.get("symbols", list()))
|
|
296
|
+
start = pd.Timestamp(info.get("start", "")).round("1s")
|
|
297
|
+
stop = pd.Timestamp(info.get("stop", "")).round("1s")
|
|
298
|
+
dscr = info.get("description", "")
|
|
299
|
+
created = pd.Timestamp(info.get("creation_time", "")).round("1s")
|
|
300
|
+
metrics = info.get("performance", {})
|
|
301
|
+
author = info.get("author", "")
|
|
302
|
+
_s = f"{yellow(str(info.get('idx')))} - {red(name)} ::: {magenta(created)} by {cyan(author)}"
|
|
303
|
+
|
|
304
|
+
_one_line_dscr = ""
|
|
305
|
+
if dscr:
|
|
306
|
+
dscr = dscr.split("\n")
|
|
307
|
+
for _d in dscr:
|
|
308
|
+
_s += f"\n\t{magenta('# ' + _d)}"
|
|
309
|
+
_one_line_dscr += "\u25cf " + _d + "\n"
|
|
310
|
+
|
|
311
|
+
_s += f"\n\tstrategy: {green(s_cls)}"
|
|
312
|
+
_s += f"\n\tinterval: {blue(start)} - {blue(stop)}"
|
|
313
|
+
_s += f"\n\tcapital: {blue(info.get('capital', ''))} {info.get('base_currency', '')} ({info.get('commissions', '')})"
|
|
314
|
+
_s += f"\n\tinstruments: {blue(smbs)}"
|
|
315
|
+
if params:
|
|
316
|
+
formats = ["{" + f":<{i}" + "}" for i in [50]]
|
|
317
|
+
_p = pd.DataFrame.from_dict(info.get("parameters", {}), orient="index")
|
|
318
|
+
for i in _p.to_string(
|
|
319
|
+
max_colwidth=30,
|
|
320
|
+
header=False,
|
|
321
|
+
formatters=[(lambda x: cyan(fmt.format(str(x)))) for fmt in formats],
|
|
322
|
+
justify="left",
|
|
323
|
+
).split("\n"):
|
|
324
|
+
_s += f"\n\t | {yellow(i)}"
|
|
300
325
|
|
|
301
|
-
if with_metrics:
|
|
302
|
-
_m_repr = (
|
|
303
|
-
pd.DataFrame.from_dict(metrics, orient="index")
|
|
304
|
-
.T[["gain", "cagr", "sharpe", "qr", "max_dd_pct", "mdd_usd", "fees", "execs"]]
|
|
305
|
-
.astype(float)
|
|
306
|
-
)
|
|
307
|
-
_m_repr = _m_repr.round(3).to_string(index=False)
|
|
308
|
-
_h, _v = _m_repr.split("\n")
|
|
309
326
|
if not as_table:
|
|
310
|
-
print(
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
327
|
+
print(_s)
|
|
328
|
+
|
|
329
|
+
if with_metrics:
|
|
330
|
+
_m_repr = (
|
|
331
|
+
pd.DataFrame.from_dict(metrics, orient="index")
|
|
332
|
+
.T[["gain", "cagr", "sharpe", "qr", "max_dd_pct", "mdd_usd", "fees", "execs"]]
|
|
333
|
+
.astype(float)
|
|
334
|
+
)
|
|
335
|
+
_m_repr = _m_repr.round(3).to_string(index=False)
|
|
336
|
+
_h, _v = _m_repr.split("\n")
|
|
337
|
+
if not as_table:
|
|
338
|
+
print("\t " + red(_h))
|
|
339
|
+
print("\t " + cyan(_v))
|
|
340
|
+
|
|
341
|
+
if not as_table:
|
|
342
|
+
print()
|
|
343
|
+
else:
|
|
344
|
+
metrics = {
|
|
345
|
+
m: round(v, 3)
|
|
346
|
+
for m, v in metrics.items()
|
|
347
|
+
if m in ["gain", "cagr", "sharpe", "qr", "max_dd_pct", "mdd_usd", "fees", "execs"]
|
|
348
|
+
}
|
|
349
|
+
_t_rep.append(
|
|
350
|
+
{"Index": info.get("idx", ""), "Strategy": name}
|
|
351
|
+
| metrics
|
|
352
|
+
| {
|
|
353
|
+
"start": start,
|
|
354
|
+
"stop": stop,
|
|
355
|
+
"Created": created,
|
|
356
|
+
"Author": author,
|
|
357
|
+
"Description": _one_line_dscr,
|
|
358
|
+
},
|
|
359
|
+
)
|
|
332
360
|
|
|
333
361
|
# - variations (only if not as_table for the time being)
|
|
334
362
|
if not as_table and show_variations:
|
|
@@ -358,11 +386,12 @@ class BacktestsResultsManager:
|
|
|
358
386
|
_m_repr = _m_repr.to_string(index=True)
|
|
359
387
|
|
|
360
388
|
print(_s)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
389
|
+
if show_each_variation_run:
|
|
390
|
+
for _i, _l in enumerate(_m_repr.split("\n")):
|
|
391
|
+
if _i == 0:
|
|
392
|
+
print("\t " + red(_l))
|
|
393
|
+
else:
|
|
394
|
+
print("\t " + blue(_l))
|
|
366
395
|
|
|
367
396
|
if as_table:
|
|
368
397
|
_df = pd.DataFrame.from_records(_t_rep, index="Index")
|
|
@@ -376,3 +405,105 @@ class BacktestsResultsManager:
|
|
|
376
405
|
.replace("<td>", '<td align="left" valign="top">')
|
|
377
406
|
)
|
|
378
407
|
return _df
|
|
408
|
+
|
|
409
|
+
def variation_plot(self, variation_idx: int, criterion: str = "sharpe", ascending: bool = False, n=3, h=600):
|
|
410
|
+
"""
|
|
411
|
+
Plot a variation of a backtest result.
|
|
412
|
+
|
|
413
|
+
Args:
|
|
414
|
+
- variation_idx (int): The index of the variation to plot.
|
|
415
|
+
- criterion (str): The criterion to plot (e.g. "sharpe", "mdd_usd", "max_dd_pct", etc.).
|
|
416
|
+
- ascending (bool): Whether to sort the results in ascending order.
|
|
417
|
+
- n (int): The number of decimal places to display.
|
|
418
|
+
- h (int): The height of the plot.
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
plotly.graph_objects.Figure: The plot of the variation.
|
|
422
|
+
"""
|
|
423
|
+
import plotly.express as px
|
|
424
|
+
from itertools import cycle
|
|
425
|
+
from qubx.utils.misc import string_shortener
|
|
426
|
+
|
|
427
|
+
_vars = self.variations.get(variation_idx)
|
|
428
|
+
if not _vars:
|
|
429
|
+
raise ValueError(f"No variations found for index {variation_idx} !")
|
|
430
|
+
|
|
431
|
+
variations = _vars.get("variations", [])
|
|
432
|
+
name = _vars.get("name", "") or ""
|
|
433
|
+
|
|
434
|
+
_r, _p = {}, {}
|
|
435
|
+
for i, v in enumerate(variations):
|
|
436
|
+
_p[i] = v["parameters"]
|
|
437
|
+
_pp = pd.DataFrame.from_records(_p).T
|
|
438
|
+
# - changed parameters
|
|
439
|
+
_cp = []
|
|
440
|
+
for c in _pp.columns:
|
|
441
|
+
if len(_pp[c].astype(str).unique()) > 1:
|
|
442
|
+
_cp.append(c)
|
|
443
|
+
|
|
444
|
+
# - if nothing was actually changed in parameters, raise an error
|
|
445
|
+
if not _cp:
|
|
446
|
+
raise ValueError(f"No variable parameters found for simulation {name} !")
|
|
447
|
+
|
|
448
|
+
_ms = max([len(string_shortener(x)) for x in _cp]) + 3
|
|
449
|
+
_h = "".join([string_shortener(x).center(_ms) for x in _cp])
|
|
450
|
+
|
|
451
|
+
_sel = lambda ds, _cp: "".join(
|
|
452
|
+
[
|
|
453
|
+
f"<span style='color:{c}'> {str(ds[k]).center(_ms)}</span>"
|
|
454
|
+
for k, c in zip(_cp, cycle(px.colors.qualitative.Plotly))
|
|
455
|
+
if k in k in ds
|
|
456
|
+
]
|
|
457
|
+
)
|
|
458
|
+
for i, v in enumerate(variations):
|
|
459
|
+
_r[i] = {"name": v["name"], **v["performance"], "parameters": _sel(v["parameters"], _cp)}
|
|
460
|
+
|
|
461
|
+
t1 = pd.DataFrame.from_records(_r).T
|
|
462
|
+
if criterion not in t1.columns:
|
|
463
|
+
raise ValueError(f"Criterion {criterion} not found in results: possible values are {t1.columns}")
|
|
464
|
+
t2 = t1.sort_values(criterion, ascending=ascending)
|
|
465
|
+
|
|
466
|
+
data = pd.Series([np.nan, *t2[criterion].to_list()], index=[_h, *t2["parameters"].to_list()])
|
|
467
|
+
|
|
468
|
+
figure = (
|
|
469
|
+
px.bar(data, orientation="h")
|
|
470
|
+
.update_layout(
|
|
471
|
+
title=dict(
|
|
472
|
+
text=f"{name} | <span style='color:orange'>{criterion.capitalize()}</span>",
|
|
473
|
+
),
|
|
474
|
+
xaxis=dict(tickfont=dict(family="monospace", size=10, color="#ff4000")),
|
|
475
|
+
yaxis=dict(
|
|
476
|
+
tickfont=dict(family="monospace", size=10, color="#40a000"),
|
|
477
|
+
dtick=1,
|
|
478
|
+
),
|
|
479
|
+
)
|
|
480
|
+
.update_layout(
|
|
481
|
+
height=h,
|
|
482
|
+
hovermode="x unified",
|
|
483
|
+
showlegend=False,
|
|
484
|
+
hoverdistance=1,
|
|
485
|
+
yaxis={"hoverformat": f".{n}f"},
|
|
486
|
+
dragmode="zoom",
|
|
487
|
+
newshape=dict(line_color="red", line_width=1.0),
|
|
488
|
+
modebar_add=["drawline", "drawopenpath", "drawrect", "eraseshape"],
|
|
489
|
+
hoverlabel=dict(align="auto", bgcolor="rgba(10, 10, 10, 0.5)"),
|
|
490
|
+
)
|
|
491
|
+
.update_xaxes(
|
|
492
|
+
showspikes=True,
|
|
493
|
+
spikemode="across",
|
|
494
|
+
spikesnap="cursor",
|
|
495
|
+
spikecolor="#306020",
|
|
496
|
+
spikethickness=1,
|
|
497
|
+
spikedash="dot",
|
|
498
|
+
title=criterion,
|
|
499
|
+
)
|
|
500
|
+
.update_yaxes(
|
|
501
|
+
spikesnap="cursor",
|
|
502
|
+
spikecolor="#306020",
|
|
503
|
+
tickformat=f".{n}f",
|
|
504
|
+
spikethickness=1,
|
|
505
|
+
title="Parameters",
|
|
506
|
+
autorange="reversed",
|
|
507
|
+
)
|
|
508
|
+
)
|
|
509
|
+
return figure
|
|
@@ -439,7 +439,7 @@ class SimulationRunner:
|
|
|
439
439
|
if isinstance(commissions, (str, type(None))):
|
|
440
440
|
commissions = {e: commissions for e in exchanges}
|
|
441
441
|
for exchange in exchanges:
|
|
442
|
-
_exchange_to_tcc[exchange] = lookup.
|
|
442
|
+
_exchange_to_tcc[exchange] = lookup.find_fees(exchange.lower(), commissions.get(exchange))
|
|
443
443
|
return _exchange_to_tcc
|
|
444
444
|
|
|
445
445
|
def _construct_account_processor(
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import re
|
|
1
2
|
from dataclasses import dataclass, field
|
|
2
3
|
from datetime import datetime
|
|
3
4
|
from enum import StrEnum
|
|
@@ -176,6 +177,14 @@ class MarketType(StrEnum):
|
|
|
176
177
|
|
|
177
178
|
@dataclass
|
|
178
179
|
class Instrument:
|
|
180
|
+
"""
|
|
181
|
+
Instrument class.
|
|
182
|
+
|
|
183
|
+
- 2025-06-11: Important change for FUTURE type: now instrument's symbol contains delivery date in format YYYYMMDD.
|
|
184
|
+
So now for let's say september's BTCUSDT future, symbol would be BTCUSD.20250914
|
|
185
|
+
and full id is `BINANCE.UM:FUTURE:BTCUSD.20250914`
|
|
186
|
+
"""
|
|
187
|
+
|
|
179
188
|
symbol: str
|
|
180
189
|
asset_type: AssetType
|
|
181
190
|
market_type: MarketType
|
|
@@ -192,8 +201,10 @@ class Instrument:
|
|
|
192
201
|
maint_margin: float = 0.0 # maintenance margin
|
|
193
202
|
liquidation_fee: float = 0.0 # liquidation fee
|
|
194
203
|
contract_size: float = 1.0 # contract size
|
|
195
|
-
onboard_date: datetime | None = None
|
|
196
|
-
delivery_date: datetime | None = None
|
|
204
|
+
onboard_date: datetime | None = None # date when instrument was listed on the exchange
|
|
205
|
+
delivery_date: datetime | None = None # date when instrument is delivered
|
|
206
|
+
delist_date: datetime | None = None # date when instrument is delisted
|
|
207
|
+
inverse: bool = False # if true, then the future is inverse
|
|
197
208
|
|
|
198
209
|
@property
|
|
199
210
|
def price_precision(self):
|
|
@@ -437,9 +448,6 @@ class AssetBalance:
|
|
|
437
448
|
return self
|
|
438
449
|
|
|
439
450
|
|
|
440
|
-
MARKET_TYPE = Literal["SPOT", "MARGIN", "SWAP", "FUTURES", "OPTION"]
|
|
441
|
-
|
|
442
|
-
|
|
443
451
|
class Position:
|
|
444
452
|
instrument: Instrument # instrument for this position
|
|
445
453
|
quantity: float = 0.0 # quantity positive for long and negative for short
|
|
@@ -878,3 +886,101 @@ class RestoredState:
|
|
|
878
886
|
balances: dict[str, AssetBalance]
|
|
879
887
|
instrument_to_target_positions: dict[Instrument, list[TargetPosition]]
|
|
880
888
|
positions: dict[Instrument, Position]
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
class InstrumentsLookup:
|
|
892
|
+
def get_lookup(self) -> dict[str, Instrument]: ...
|
|
893
|
+
|
|
894
|
+
def find(
|
|
895
|
+
self,
|
|
896
|
+
exchange: str,
|
|
897
|
+
base: str,
|
|
898
|
+
quote: str,
|
|
899
|
+
settle: str | None = None,
|
|
900
|
+
market_type: MarketType | None = None,
|
|
901
|
+
) -> Instrument | None:
|
|
902
|
+
for i in self.get_lookup().values():
|
|
903
|
+
if (
|
|
904
|
+
i.exchange == exchange
|
|
905
|
+
and ((i.base == base and i.quote == quote) or (i.base == quote and i.quote == base))
|
|
906
|
+
and (market_type is None or i.market_type == market_type)
|
|
907
|
+
):
|
|
908
|
+
if settle is not None and i.settle is not None:
|
|
909
|
+
if i.settle == settle:
|
|
910
|
+
return i
|
|
911
|
+
else:
|
|
912
|
+
return i
|
|
913
|
+
return None
|
|
914
|
+
|
|
915
|
+
def find_symbol(self, exchange: str, symbol: str, market_type: MarketType | None = None) -> Instrument | None:
|
|
916
|
+
for i in self.get_lookup().values():
|
|
917
|
+
if (
|
|
918
|
+
(i.exchange == exchange)
|
|
919
|
+
and (i.symbol == symbol)
|
|
920
|
+
and (market_type is None or i.market_type == market_type)
|
|
921
|
+
):
|
|
922
|
+
return i
|
|
923
|
+
|
|
924
|
+
return None
|
|
925
|
+
|
|
926
|
+
def find_instruments(
|
|
927
|
+
self,
|
|
928
|
+
exchange: str,
|
|
929
|
+
quote: str | None = None,
|
|
930
|
+
market_type: MarketType | None = None,
|
|
931
|
+
as_of: str | pd.Timestamp | None = None,
|
|
932
|
+
) -> list[Instrument]:
|
|
933
|
+
"""
|
|
934
|
+
Find instruments by exchange, quote, market type and as of date.
|
|
935
|
+
If as_of is not None, then only instruments that are not delisted after as_of date will be returned.
|
|
936
|
+
- exchange: str - exchange name
|
|
937
|
+
- quote: str | None - quote currency
|
|
938
|
+
- market_type: MarketType | None - market type
|
|
939
|
+
- as_of is a string in format YYYY-MM-DD or pd.Timestamp or None
|
|
940
|
+
"""
|
|
941
|
+
_limit_time = pd.Timestamp(as_of) if as_of else None
|
|
942
|
+
return [
|
|
943
|
+
i
|
|
944
|
+
for i in self.get_lookup().values()
|
|
945
|
+
if i.exchange == exchange
|
|
946
|
+
and (quote is None or i.quote == quote)
|
|
947
|
+
and (market_type is None or i.market_type == market_type)
|
|
948
|
+
and (
|
|
949
|
+
_limit_time is None
|
|
950
|
+
or (i.delist_date is None)
|
|
951
|
+
or (pd.Timestamp(i.delist_date).tz_localize(None) >= _limit_time)
|
|
952
|
+
)
|
|
953
|
+
]
|
|
954
|
+
|
|
955
|
+
def find_aux_instrument_for(
|
|
956
|
+
self, instrument: Instrument, base_currency: str, market_type: MarketType | None = None
|
|
957
|
+
) -> Instrument | None:
|
|
958
|
+
"""
|
|
959
|
+
Tries to find aux instrument (for conversions to funded currency)
|
|
960
|
+
for example:
|
|
961
|
+
ETHBTC -> BTCUSDT for base_currency USDT
|
|
962
|
+
EURGBP -> GBPUSD for base_currency USD
|
|
963
|
+
...
|
|
964
|
+
"""
|
|
965
|
+
if market_type is None:
|
|
966
|
+
market_type = instrument.market_type
|
|
967
|
+
base_currency = base_currency.upper()
|
|
968
|
+
if instrument.quote != base_currency:
|
|
969
|
+
return self.find(instrument.exchange, instrument.quote, base_currency, market_type=market_type)
|
|
970
|
+
return None
|
|
971
|
+
|
|
972
|
+
def __getitem__(self, spath: str) -> list[Instrument]:
|
|
973
|
+
"""
|
|
974
|
+
Helper method for finding instruments by pattern.
|
|
975
|
+
It's convenient to use in research mode.
|
|
976
|
+
"""
|
|
977
|
+
res = []
|
|
978
|
+
c = re.compile(spath)
|
|
979
|
+
for k, v in self.get_lookup().items():
|
|
980
|
+
if re.match(c, k):
|
|
981
|
+
res.append(v)
|
|
982
|
+
return res
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
class FeesLookup:
|
|
986
|
+
def find_fees(self, exchange: str, spec: str | None) -> TransactionCostsCalculator: ...
|
|
@@ -6,15 +6,15 @@ import time
|
|
|
6
6
|
from collections import defaultdict, deque
|
|
7
7
|
from inspect import isbuiltin, isclass, isfunction, ismethod, ismethoddescriptor
|
|
8
8
|
from threading import Thread
|
|
9
|
-
from typing import Any, Callable
|
|
9
|
+
from typing import Any, Callable
|
|
10
10
|
|
|
11
11
|
import numpy as np
|
|
12
12
|
import pandas as pd
|
|
13
13
|
from croniter import croniter
|
|
14
14
|
|
|
15
15
|
from qubx import logger
|
|
16
|
-
from qubx.core.basics import SW, CtrlChannel, DataType, Instrument, Timestamped
|
|
17
|
-
from qubx.core.series import OHLCV, Bar, OrderBook, Quote, Trade
|
|
16
|
+
from qubx.core.basics import SW, CtrlChannel, DataType, Instrument, Timestamped, dt_64
|
|
17
|
+
from qubx.core.series import OHLCV, Bar, OrderBook, Quote, Trade, time_as_nsec
|
|
18
18
|
from qubx.utils.time import convert_seconds_to_str, convert_tf_str_td64, interval_to_cron
|
|
19
19
|
|
|
20
20
|
|
|
@@ -26,9 +26,9 @@ class CachedMarketDataHolder:
|
|
|
26
26
|
default_timeframe: np.timedelta64
|
|
27
27
|
_last_bar: dict[Instrument, Bar | None]
|
|
28
28
|
_ohlcvs: dict[Instrument, dict[np.timedelta64, OHLCV]]
|
|
29
|
-
_updates: dict[Instrument,
|
|
29
|
+
_updates: dict[Instrument, Bar | Quote | Trade]
|
|
30
30
|
|
|
31
|
-
_instr_to_sub_to_buffer:
|
|
31
|
+
_instr_to_sub_to_buffer: dict[Instrument, dict[str, deque]]
|
|
32
32
|
|
|
33
33
|
def __init__(self, default_timeframe: str | None = None, max_buffer_size: int = 10_000) -> None:
|
|
34
34
|
self._ohlcvs = dict()
|
|
@@ -93,7 +93,7 @@ class CachedMarketDataHolder:
|
|
|
93
93
|
|
|
94
94
|
return self._ohlcvs[instrument][tf]
|
|
95
95
|
|
|
96
|
-
def get_data(self, instrument: Instrument, event_type: str) ->
|
|
96
|
+
def get_data(self, instrument: Instrument, event_type: str) -> list[Any]:
|
|
97
97
|
return list(self._instr_to_sub_to_buffer[instrument][event_type])
|
|
98
98
|
|
|
99
99
|
def update(
|
|
@@ -126,7 +126,7 @@ class CachedMarketDataHolder:
|
|
|
126
126
|
pass
|
|
127
127
|
|
|
128
128
|
@SW.watch("CachedMarketDataHolder")
|
|
129
|
-
def update_by_bars(self, instrument: Instrument, timeframe: str | np.timedelta64, bars:
|
|
129
|
+
def update_by_bars(self, instrument: Instrument, timeframe: str | np.timedelta64, bars: list[Bar]) -> OHLCV:
|
|
130
130
|
"""
|
|
131
131
|
Update or create OHLCV series with the provided historical bars.
|
|
132
132
|
|
|
@@ -203,6 +203,17 @@ class CachedMarketDataHolder:
|
|
|
203
203
|
continue
|
|
204
204
|
ser.update(trade.time, trade.price, total_vol, bought_vol)
|
|
205
205
|
|
|
206
|
+
def finalize_all_ohlc(self, time: dt_64):
|
|
207
|
+
"""
|
|
208
|
+
Finalize all OHLCV series at the given time.
|
|
209
|
+
FIXME: (2025-06-17) This is part of urgent live fix and must be removed in future !!!.
|
|
210
|
+
"""
|
|
211
|
+
for instrument in self._ohlcvs.keys():
|
|
212
|
+
# - use most recent update
|
|
213
|
+
if (_u := self._updates.get(instrument)) is not None:
|
|
214
|
+
_px = extract_price(_u)
|
|
215
|
+
self.update_by_bar(instrument, Bar(time_as_nsec(time), _px, _px, _px, _px, 0, 0))
|
|
216
|
+
|
|
206
217
|
|
|
207
218
|
SPEC_REGEX = re.compile(
|
|
208
219
|
r"((?P<type>[A-Za-z]+)(\.?(?P<timeframe>[0-9A-Za-z]+))?\ *:)?"
|