Qubx 0.5.1__tar.gz → 0.5.5__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.5.1 → qubx-0.5.5}/PKG-INFO +2 -1
- {qubx-0.5.1 → qubx-0.5.5}/pyproject.toml +3 -2
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/__init__.py +2 -2
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/_nb_magic.py +1 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/backtester/__init__.py +2 -1
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/backtester/account.py +5 -7
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/backtester/data.py +58 -38
- qubx-0.5.5/src/qubx/backtester/management.py +141 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/backtester/ome.py +8 -11
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/backtester/optimization.py +19 -12
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/backtester/simulated_data.py +115 -73
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/backtester/simulator.py +137 -126
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/backtester/utils.py +105 -16
- qubx-0.5.5/src/qubx/cli/commands.py +67 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/connectors/ccxt/customizations.py +1 -3
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/account.py +6 -4
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/basics.py +18 -10
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/context.py +23 -19
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/exceptions.py +2 -2
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/helpers.py +38 -17
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/interfaces.py +74 -8
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/loggers.py +1 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/lookups.py +98 -17
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/metrics.py +129 -30
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/mixins/market.py +11 -4
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/mixins/processing.py +25 -27
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/mixins/subscription.py +14 -14
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/mixins/trading.py +3 -2
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/mixins/universe.py +2 -5
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/series.pyi +1 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/series.pyx +13 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/gathering/simplest.py +5 -6
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/math/stats.py +29 -6
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/pandaz/ta.py +6 -9
- qubx-0.5.5/src/qubx/resources/instruments/symbols-binance.cm.json +1 -0
- qubx-0.5.5/src/qubx/resources/instruments/symbols-binance.json +1 -0
- qubx-0.5.5/src/qubx/resources/instruments/symbols-binance.um.json +1 -0
- qubx-0.5.5/src/qubx/resources/instruments/symbols-bitfinex.f.json +1 -0
- qubx-0.5.5/src/qubx/resources/instruments/symbols-bitfinex.json +1 -0
- qubx-0.5.5/src/qubx/resources/instruments/symbols-kraken.f.json +1 -0
- qubx-0.5.5/src/qubx/resources/instruments/symbols-kraken.json +1 -0
- qubx-0.5.5/src/qubx/trackers/abvanced.py +236 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/trackers/composite.py +2 -2
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/trackers/riskctrl.py +126 -80
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/trackers/sizers.py +71 -27
- qubx-0.5.5/src/qubx/utils/__init__.py +5 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/utils/charting/lookinglass.py +36 -75
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/utils/charting/mpl_helpers.py +26 -12
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/utils/marketdata/ccxt.py +3 -1
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/utils/misc.py +77 -15
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/utils/orderbook.py +9 -9
- {qubx-0.5.1/src/qubx → qubx-0.5.5/src/qubx/utils}/plotting/dashboard.py +1 -2
- qubx-0.5.5/src/qubx/utils/runner/__init__.py +1 -0
- {qubx-0.5.1/src/qubx/utils → qubx-0.5.5/src/qubx/utils/runner}/_jupyter_runner.pyt +4 -3
- qubx-0.5.5/src/qubx/utils/runner/accounts.py +88 -0
- qubx-0.5.5/src/qubx/utils/runner/configs.py +65 -0
- qubx-0.5.5/src/qubx/utils/runner/runner.py +459 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/utils/time.py +15 -11
- qubx-0.5.1/src/qubx/utils/__init__.py +0 -4
- qubx-0.5.1/src/qubx/utils/runner.py +0 -543
- {qubx-0.5.1 → qubx-0.5.5}/README.md +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/build.py +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/backtester/broker.py +0 -0
- {qubx-0.5.1/src/qubx/connectors/ccxt → qubx-0.5.5/src/qubx/cli}/__init__.py +0 -0
- {qubx-0.5.1/src/qubx/core → qubx-0.5.5/src/qubx/connectors/ccxt}/__init__.py +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/connectors/ccxt/account.py +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/connectors/ccxt/broker.py +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/connectors/ccxt/data.py +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/connectors/ccxt/factory.py +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/connectors/ccxt/utils.py +0 -0
- {qubx-0.5.1/src/qubx/plotting → qubx-0.5.5/src/qubx/core}/__init__.py +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/series.pxd +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/utils.pyi +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/core/utils.pyx +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/data/__init__.py +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/data/helpers.py +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/data/readers.py +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/data/tardis.py +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/math/__init__.py +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/pandaz/utils.py +0 -0
- {qubx-0.5.1/src/qubx/plotting/renderers → qubx-0.5.5/src/qubx/ta}/__init__.py +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/trackers/__init__.py +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/utils/ntp.py +0 -0
- {qubx-0.5.1 → qubx-0.5.5}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-0.5.1/src/qubx/ta → qubx-0.5.5/src/qubx/utils/plotting}/__init__.py +0 -0
- {qubx-0.5.1/src/qubx → qubx-0.5.5/src/qubx/utils}/plotting/data.py +0 -0
- {qubx-0.5.1/src/qubx → qubx-0.5.5/src/qubx/utils}/plotting/interfaces.py +0 -0
- /qubx-0.5.1/src/qubx/connectors/ccxt/ccxt_connector.py → /qubx-0.5.5/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-0.5.1/src/qubx → qubx-0.5.5/src/qubx/utils}/plotting/renderers/plotly.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: Qubx
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.5
|
|
4
4
|
Summary: Qubx - quantitative trading framework
|
|
5
5
|
Home-page: https://github.com/dmarienko/Qubx
|
|
6
6
|
Author: Dmitry Marienko
|
|
@@ -40,6 +40,7 @@ Requires-Dist: sortedcontainers (>=2.4.0,<3.0.0)
|
|
|
40
40
|
Requires-Dist: stackprinter (>=0.2.10,<0.3.0)
|
|
41
41
|
Requires-Dist: statsmodels (>=0.14.2,<0.15.0)
|
|
42
42
|
Requires-Dist: tabulate (>=0.9.0,<0.10.0)
|
|
43
|
+
Requires-Dist: toml (>=0.10.2,<0.11.0)
|
|
43
44
|
Requires-Dist: tqdm
|
|
44
45
|
Project-URL: Repository, https://github.com/dmarienko/Qubx
|
|
45
46
|
Description-Content-Type: text/markdown
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "Qubx"
|
|
3
|
-
version = "0.5.
|
|
3
|
+
version = "0.5.5"
|
|
4
4
|
description = "Qubx - quantitative trading framework"
|
|
5
5
|
authors = [
|
|
6
6
|
"Dmitry Marienko <dmitry@gmail.com>",
|
|
@@ -48,6 +48,7 @@ dash = "^2.18.2"
|
|
|
48
48
|
dash-bootstrap-components = "^1.6.0"
|
|
49
49
|
tabulate = "^0.9.0"
|
|
50
50
|
jupyter-console = "^6.6.3"
|
|
51
|
+
toml = "^0.10.2"
|
|
51
52
|
|
|
52
53
|
[tool.poetry.group.dev.dependencies]
|
|
53
54
|
pre-commit = "^2.20.0"
|
|
@@ -95,4 +96,4 @@ line-length = 120
|
|
|
95
96
|
"*.ipynb" = ["F405", "F401", "E701", "E402", "F403", "E401", "E702"]
|
|
96
97
|
|
|
97
98
|
[tool.poetry.scripts]
|
|
98
|
-
qubx = "qubx.
|
|
99
|
+
qubx = "qubx.cli.commands:main"
|
|
@@ -37,7 +37,7 @@ def formatter(record):
|
|
|
37
37
|
class QubxLogConfig:
|
|
38
38
|
@staticmethod
|
|
39
39
|
def get_log_level():
|
|
40
|
-
return os.getenv("QUBX_LOG_LEVEL", "
|
|
40
|
+
return os.getenv("QUBX_LOG_LEVEL", "WARNING")
|
|
41
41
|
|
|
42
42
|
@staticmethod
|
|
43
43
|
def set_log_level(level: str):
|
|
@@ -115,7 +115,7 @@ if runtime_env() in ["notebook", "shell"]:
|
|
|
115
115
|
# - temporary workaround for vscode - dark theme not applying to ipywidgets in notebook
|
|
116
116
|
# - see https://github.com/microsoft/vscode-jupyter/issues/7161
|
|
117
117
|
if runtime_env() == "notebook":
|
|
118
|
-
_vscode_clr_trick = """from IPython.display import display, HTML; display(HTML("<style> .cell-output-ipywidget-background { background-color: transparent !important; } :root { --jp-widgets-color: var(--vscode-editor-foreground); --jp-widgets-font-size: var(--vscode-editor-font-size); } </style>"))"""
|
|
118
|
+
_vscode_clr_trick = """from IPython.display import display, HTML; display(HTML("<style> .cell-output-ipywidget-background { background-color: transparent !important; } :root { --jp-widgets-color: var(--vscode-editor-foreground); --jp-widgets-font-size: var(--vscode-editor-font-size); } .widget-hprogress, .jupyter-widget-hprogress { height: 16px; align-self: center; kj} table.dataframe, .dataframe td, .dataframe tr { border: 1px solid #55554a85; border-collapse: collapse; color: #859548d9 !important; } .dataframe th { border: 1px solid #55554a85; border-collapse: collapse; background-color: #010101 !important; color: #177 !important; } </style>"))"""
|
|
119
119
|
exec(_vscode_clr_trick, self.shell.user_ns)
|
|
120
120
|
|
|
121
121
|
elif "light" in line.lower():
|
|
@@ -86,6 +86,7 @@ if runtime_env() in ["notebook", "shell"]:
|
|
|
86
86
|
)
|
|
87
87
|
from qubx.utils.charting.lookinglass import LookingGlass
|
|
88
88
|
from qubx.utils.charting.mpl_helpers import fig, ohlc_plot, plot_trends, sbp, subplot
|
|
89
|
+
from qubx.utils.misc import this_project_root
|
|
89
90
|
|
|
90
91
|
# - setup short numpy output format
|
|
91
92
|
np_fmt_short()
|
|
@@ -3,7 +3,6 @@ from qubx.backtester.ome import OrdersManagementEngine
|
|
|
3
3
|
from qubx.core.account import BasicAccountProcessor
|
|
4
4
|
from qubx.core.basics import (
|
|
5
5
|
ZERO_COSTS,
|
|
6
|
-
BatchEvent,
|
|
7
6
|
CtrlChannel,
|
|
8
7
|
Instrument,
|
|
9
8
|
Order,
|
|
@@ -47,7 +46,7 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
|
|
|
47
46
|
self._half_tick_size = {}
|
|
48
47
|
self._fill_stop_order_at_price = accurate_stop_orders_execution
|
|
49
48
|
if self._fill_stop_order_at_price:
|
|
50
|
-
logger.info(f"{self.__class__.__name__} emulates stop orders executions at exact price")
|
|
49
|
+
logger.info(f"[<y>{self.__class__.__name__}</y>] :: emulates stop orders executions at exact price")
|
|
51
50
|
|
|
52
51
|
def get_orders(self, instrument: Instrument | None = None) -> list[Order]:
|
|
53
52
|
if instrument is not None:
|
|
@@ -103,31 +102,30 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
|
|
|
103
102
|
return super().process_order(order, update_locked_value)
|
|
104
103
|
|
|
105
104
|
def emulate_quote_from_data(
|
|
106
|
-
self, instrument: Instrument, timestamp: dt_64, data: float | Timestamped
|
|
105
|
+
self, instrument: Instrument, timestamp: dt_64, data: float | Timestamped
|
|
107
106
|
) -> Quote | None:
|
|
108
107
|
if instrument not in self._half_tick_size:
|
|
109
108
|
_ = self.get_position(instrument)
|
|
110
109
|
|
|
111
|
-
_ts2 = self._half_tick_size[instrument]
|
|
112
110
|
if isinstance(data, Quote):
|
|
113
111
|
return data
|
|
114
112
|
|
|
115
113
|
elif isinstance(data, Trade):
|
|
114
|
+
_ts2 = self._half_tick_size[instrument]
|
|
116
115
|
if data.taker: # type: ignore
|
|
117
116
|
return Quote(timestamp, data.price - _ts2 * 2, data.price, 0, 0) # type: ignore
|
|
118
117
|
else:
|
|
119
118
|
return Quote(timestamp, data.price, data.price + _ts2 * 2, 0, 0) # type: ignore
|
|
120
119
|
|
|
121
120
|
elif isinstance(data, Bar):
|
|
121
|
+
_ts2 = self._half_tick_size[instrument]
|
|
122
122
|
return Quote(timestamp, data.close - _ts2, data.close + _ts2, 0, 0) # type: ignore
|
|
123
123
|
|
|
124
124
|
elif isinstance(data, OrderBook):
|
|
125
125
|
return data.to_quote()
|
|
126
126
|
|
|
127
|
-
elif isinstance(data, BatchEvent):
|
|
128
|
-
return self.emulate_quote_from_data(instrument, timestamp, data.data[-1])
|
|
129
|
-
|
|
130
127
|
elif isinstance(data, float):
|
|
128
|
+
_ts2 = self._half_tick_size[instrument]
|
|
131
129
|
return Quote(timestamp, data - _ts2, data + _ts2, 0, 0)
|
|
132
130
|
|
|
133
131
|
else:
|
|
@@ -6,13 +6,14 @@ import pandas as pd
|
|
|
6
6
|
from tqdm.auto import tqdm
|
|
7
7
|
|
|
8
8
|
from qubx import logger
|
|
9
|
-
from qubx.backtester.simulated_data import
|
|
9
|
+
from qubx.backtester.simulated_data import IterableSimulationData
|
|
10
10
|
from qubx.core.basics import (
|
|
11
11
|
CtrlChannel,
|
|
12
12
|
DataType,
|
|
13
13
|
Instrument,
|
|
14
14
|
TimestampedDict,
|
|
15
15
|
)
|
|
16
|
+
from qubx.core.exceptions import SimulationError
|
|
16
17
|
from qubx.core.helpers import BasicScheduler
|
|
17
18
|
from qubx.core.interfaces import IDataProvider
|
|
18
19
|
from qubx.core.series import Bar, Quote, time_as_nsec
|
|
@@ -74,23 +75,22 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
74
75
|
start: str | pd.Timestamp,
|
|
75
76
|
end: str | pd.Timestamp,
|
|
76
77
|
silent: bool = False,
|
|
77
|
-
enable_event_batching: bool = True,
|
|
78
78
|
) -> None:
|
|
79
79
|
logger.info(f"{self.__class__.__name__} ::: Simulation started at {start} :::")
|
|
80
80
|
|
|
81
81
|
if self._pregenerated_signals:
|
|
82
82
|
self._prepare_generated_signals(start, end)
|
|
83
|
-
_run = self.
|
|
84
|
-
enable_event_batching = False # no batching for pre-generated signals
|
|
83
|
+
_run = self._process_generated_signals
|
|
85
84
|
else:
|
|
86
|
-
_run = self.
|
|
85
|
+
_run = self._process_strategy
|
|
87
86
|
|
|
88
|
-
qiter = EventBatcher(self._data_source.create_iterable(start, end), passthrough=not enable_event_batching)
|
|
89
87
|
start, end = pd.Timestamp(start), pd.Timestamp(end)
|
|
90
88
|
total_duration = end - start
|
|
91
89
|
update_delta = total_duration / 100
|
|
92
90
|
prev_dt = pd.Timestamp(start)
|
|
93
91
|
|
|
92
|
+
# - date iteration
|
|
93
|
+
qiter = self._data_source.create_iterable(start, end)
|
|
94
94
|
if silent:
|
|
95
95
|
for instrument, data_type, event, is_hist in qiter:
|
|
96
96
|
if not _run(instrument, data_type, event, is_hist):
|
|
@@ -114,7 +114,9 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
114
114
|
logger.info(f"{self.__class__.__name__} ::: Simulation finished at {end} :::")
|
|
115
115
|
|
|
116
116
|
def set_generated_signals(self, signals: pd.Series | pd.DataFrame):
|
|
117
|
-
logger.debug(
|
|
117
|
+
logger.debug(
|
|
118
|
+
f"[<y>{self.__class__.__name__}</y>] :: Using pre-generated signals:\n {str(signals.count()).strip('ndtype: int64')}"
|
|
119
|
+
)
|
|
118
120
|
# - sanity check
|
|
119
121
|
signals.index = pd.DatetimeIndex(signals.index)
|
|
120
122
|
|
|
@@ -132,11 +134,29 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
132
134
|
return True
|
|
133
135
|
|
|
134
136
|
def subscribe(self, subscription_type: str, instruments: set[Instrument], reset: bool) -> None:
|
|
135
|
-
|
|
137
|
+
_new_instr = [i for i in instruments if not self.has_subscription(i, subscription_type)]
|
|
136
138
|
self._data_source.add_instruments_for_subscription(subscription_type, list(instruments))
|
|
137
139
|
|
|
140
|
+
# - provide historical data and last quote for subscribed instruments
|
|
141
|
+
for i in _new_instr:
|
|
142
|
+
h_data = self._data_source.peek_historical_data(i, subscription_type)
|
|
143
|
+
if h_data:
|
|
144
|
+
# _s_type = DataType.from_str(subscription_type)[0]
|
|
145
|
+
last_update = h_data[-1]
|
|
146
|
+
if last_quote := self._account.emulate_quote_from_data(i, last_update.time, last_update): # type: ignore
|
|
147
|
+
# - send historical data to the channel
|
|
148
|
+
self.channel.send((i, subscription_type, h_data, True))
|
|
149
|
+
|
|
150
|
+
# - set last quote
|
|
151
|
+
self._last_quotes[i] = last_quote
|
|
152
|
+
|
|
153
|
+
# - also need to pass this quote to OME !
|
|
154
|
+
self._account._process_new_quote(i, last_quote)
|
|
155
|
+
|
|
156
|
+
logger.debug(f" | subscribed {subscription_type} {i} -> {last_quote}")
|
|
157
|
+
|
|
138
158
|
def unsubscribe(self, subscription_type: str, instruments: set[Instrument] | Instrument | None = None) -> None:
|
|
139
|
-
logger.debug(f" | unsubscribe: {subscription_type} -> {instruments}")
|
|
159
|
+
# logger.debug(f" | unsubscribe: {subscription_type} -> {instruments}")
|
|
140
160
|
if instruments is not None:
|
|
141
161
|
self._data_source.remove_instruments_from_subscription(
|
|
142
162
|
subscription_type, [instruments] if isinstance(instruments, Instrument) else list(instruments)
|
|
@@ -147,12 +167,12 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
147
167
|
|
|
148
168
|
def get_subscriptions(self, instrument: Instrument) -> list[str]:
|
|
149
169
|
_s_lst = self._data_source.get_subscriptions_for_instrument(instrument)
|
|
150
|
-
logger.debug(f" | get_subscriptions {instrument} -> {_s_lst}")
|
|
170
|
+
# logger.debug(f" | get_subscriptions {instrument} -> {_s_lst}")
|
|
151
171
|
return _s_lst
|
|
152
172
|
|
|
153
173
|
def get_subscribed_instruments(self, subscription_type: str | None = None) -> list[Instrument]:
|
|
154
174
|
_in_lst = self._data_source.get_instruments_for_subscription(subscription_type or DataType.ALL)
|
|
155
|
-
logger.debug(f" | get_subscribed_instruments {subscription_type} -> {_in_lst}")
|
|
175
|
+
# logger.debug(f" | get_subscribed_instruments {subscription_type} -> {_in_lst}")
|
|
156
176
|
return _in_lst
|
|
157
177
|
|
|
158
178
|
def warmup(self, configs: dict[tuple[str, Instrument], str]) -> None:
|
|
@@ -190,15 +210,16 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
190
210
|
if s == i.symbol or s == str(i) or s == f"{i.exchange}:{i.symbol}" or str(s) == str(i):
|
|
191
211
|
_start, _end = pd.Timestamp(start), pd.Timestamp(end)
|
|
192
212
|
_start_idx, _end_idx = v.index.get_indexer([_start, _end], method="ffill")
|
|
193
|
-
sel = v.iloc[max(_start_idx, 0) : _end_idx + 1]
|
|
213
|
+
sel = v.iloc[max(_start_idx, 0) : _end_idx + 1]
|
|
194
214
|
|
|
215
|
+
# TODO: check if data has exec_price - it means we have deals
|
|
195
216
|
self._to_process[i] = list(zip(sel.index, sel.values))
|
|
196
217
|
_s_inst = i
|
|
197
218
|
break
|
|
198
219
|
|
|
199
220
|
if _s_inst is None:
|
|
200
221
|
logger.error(f"Can't find instrument for pregenerated signals with id '{s}'")
|
|
201
|
-
raise
|
|
222
|
+
raise SimulationError(f"Can't find instrument for pregenerated signals with id '{s}'")
|
|
202
223
|
|
|
203
224
|
def _convert_records_to_bars(
|
|
204
225
|
self, records: list[TimestampedDict], cut_time_ns: int, timeframe_ns: int
|
|
@@ -228,42 +249,41 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
228
249
|
|
|
229
250
|
return bars
|
|
230
251
|
|
|
231
|
-
def
|
|
232
|
-
|
|
233
|
-
|
|
252
|
+
def _process_generated_signals(self, instrument: Instrument, data_type: str, data: Any, is_hist: bool) -> bool:
|
|
253
|
+
cc = self.channel
|
|
254
|
+
t = np.datetime64(data.time, "ns")
|
|
234
255
|
|
|
235
|
-
|
|
236
|
-
|
|
256
|
+
if not is_hist:
|
|
257
|
+
# - signals for this instrument
|
|
258
|
+
sigs = self._to_process[instrument]
|
|
237
259
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
260
|
+
while sigs and t >= (_signal_time := sigs[0][0].as_unit("ns").asm8):
|
|
261
|
+
self.time_provider.set_time(_signal_time)
|
|
262
|
+
cc.send((instrument, "event", {"order": sigs[0][1]}, False))
|
|
263
|
+
sigs.pop(0)
|
|
241
264
|
|
|
242
|
-
|
|
265
|
+
if q := self._account.emulate_quote_from_data(instrument, t, data):
|
|
266
|
+
self._last_quotes[instrument] = q
|
|
267
|
+
|
|
268
|
+
self.time_provider.set_time(t)
|
|
243
269
|
cc.send((instrument, data_type, data, is_hist))
|
|
244
|
-
sigs = self._to_process[instrument]
|
|
245
|
-
_current_time = self.time_provider.time()
|
|
246
|
-
while sigs and sigs[0][0].as_unit("ns").asm8 <= _current_time:
|
|
247
|
-
cc.send((instrument, "event", {"order": sigs[0][1]}, is_hist))
|
|
248
|
-
sigs.pop(0)
|
|
249
270
|
|
|
250
271
|
return cc.control.is_set()
|
|
251
272
|
|
|
252
|
-
def
|
|
253
|
-
t = data.time # type: ignore
|
|
254
|
-
self.time_provider.set_time(np.datetime64(t, "ns"))
|
|
255
|
-
|
|
256
|
-
q = self._account.emulate_quote_from_data(instrument, np.datetime64(t, "ns"), data)
|
|
273
|
+
def _process_strategy(self, instrument: Instrument, data_type: str, data: Any, is_hist: bool) -> bool:
|
|
257
274
|
cc = self.channel
|
|
275
|
+
t = np.datetime64(data.time, "ns")
|
|
258
276
|
|
|
259
|
-
if not is_hist
|
|
260
|
-
self.
|
|
277
|
+
if not is_hist:
|
|
278
|
+
if t >= (_next_exp_time := self._scheduler.next_expected_event_time()):
|
|
279
|
+
# - we use exact event's time
|
|
280
|
+
self.time_provider.set_time(_next_exp_time)
|
|
281
|
+
self._scheduler.check_and_run_tasks()
|
|
261
282
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
# - push nothing - it will force to process last event
|
|
265
|
-
cc.send((None, "service_time", None, False))
|
|
283
|
+
if q := self._account.emulate_quote_from_data(instrument, t, data):
|
|
284
|
+
self._last_quotes[instrument] = q
|
|
266
285
|
|
|
286
|
+
self.time_provider.set_time(t)
|
|
267
287
|
cc.send((instrument, data_type, data, is_hist))
|
|
268
288
|
|
|
269
289
|
return cc.control.is_set()
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import zipfile
|
|
3
|
+
from collections import defaultdict
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from qubx.core.metrics import TradingSessionResult, _pfl_metrics_prepare
|
|
10
|
+
from qubx.utils.misc import blue, cyan, green, magenta, red, yellow
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BacktestsResultsManager:
|
|
14
|
+
"""
|
|
15
|
+
Manager class for handling backtesting results.
|
|
16
|
+
|
|
17
|
+
This class provides functionality to load, list and manage backtesting results stored in zip files.
|
|
18
|
+
Each result contains trading session information and metrics that can be loaded and analyzed.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
path : str
|
|
23
|
+
Path to directory containing backtesting result zip files
|
|
24
|
+
|
|
25
|
+
Methods
|
|
26
|
+
-------
|
|
27
|
+
reload()
|
|
28
|
+
Reloads all backtesting results from the specified path
|
|
29
|
+
list(regex="", with_metrics=False)
|
|
30
|
+
Lists all backtesting results, optionally filtered by regex and including metrics
|
|
31
|
+
load(name)
|
|
32
|
+
Loads a specific backtesting result by name
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(self, path: str):
|
|
36
|
+
self.path = path
|
|
37
|
+
self.reload()
|
|
38
|
+
|
|
39
|
+
def reload(self) -> "BacktestsResultsManager":
|
|
40
|
+
self.results = {}
|
|
41
|
+
names = defaultdict(lambda: 0)
|
|
42
|
+
for p in Path(self.path).glob("**/*.zip"):
|
|
43
|
+
with zipfile.ZipFile(p, "r") as zip_ref:
|
|
44
|
+
try:
|
|
45
|
+
info = yaml.safe_load(zip_ref.read("info.yml"))
|
|
46
|
+
info["path"] = str(p)
|
|
47
|
+
n = info.get("name", "")
|
|
48
|
+
_new_name = n if names[n] == 0 else f"{n}.{names[n]}"
|
|
49
|
+
names[n] += 1
|
|
50
|
+
info["name"] = _new_name
|
|
51
|
+
self.results[_new_name] = info
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
# - reindex
|
|
56
|
+
_idx = 1
|
|
57
|
+
for n in sorted(self.results.keys()):
|
|
58
|
+
self.results[n]["idx"] = _idx
|
|
59
|
+
_idx += 1
|
|
60
|
+
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
def load(self, name: str | int | list[int] | list[str]) -> TradingSessionResult | list[TradingSessionResult]:
|
|
64
|
+
for info in self.results.values():
|
|
65
|
+
match name:
|
|
66
|
+
case int():
|
|
67
|
+
if info.get("idx", -1) == name:
|
|
68
|
+
return TradingSessionResult.from_file(info["path"])
|
|
69
|
+
case str():
|
|
70
|
+
if info.get("name", "") == name:
|
|
71
|
+
return TradingSessionResult.from_file(info["path"])
|
|
72
|
+
case list():
|
|
73
|
+
return [self.load(i) for i in name]
|
|
74
|
+
|
|
75
|
+
raise ValueError(f"No result found for {name}")
|
|
76
|
+
|
|
77
|
+
def list(self, regex: str = "", with_metrics=False, params=False):
|
|
78
|
+
for n in sorted(self.results.keys()):
|
|
79
|
+
info = self.results[n]
|
|
80
|
+
s_cls = info.get("strategy_class", "").split(".")[-1]
|
|
81
|
+
|
|
82
|
+
if regex:
|
|
83
|
+
if not re.match(regex, n, re.IGNORECASE):
|
|
84
|
+
if not re.match(regex, s_cls, re.IGNORECASE):
|
|
85
|
+
continue
|
|
86
|
+
|
|
87
|
+
name = info.get("name", "")
|
|
88
|
+
smbs = ", ".join(info.get("symbols", list()))
|
|
89
|
+
start = pd.Timestamp(info.get("start", "")).round("1s")
|
|
90
|
+
stop = pd.Timestamp(info.get("stop", "")).round("1s")
|
|
91
|
+
dscr = info.get("description", "")
|
|
92
|
+
_s = f"{yellow(str(info.get('idx')))} - {red(name)} ::: {magenta(pd.Timestamp(info.get('creation_time', '')).round('1s'))} by {cyan(info.get('author', ''))}"
|
|
93
|
+
|
|
94
|
+
if dscr:
|
|
95
|
+
dscr = dscr.split("\n")
|
|
96
|
+
for _d in dscr:
|
|
97
|
+
_s += f"\n\t{magenta('# ' + _d)}"
|
|
98
|
+
|
|
99
|
+
_s += f"\n\tstrategy: {green(s_cls)}"
|
|
100
|
+
_s += f"\n\tinterval: {blue(start)} - {blue(stop)}"
|
|
101
|
+
_s += f"\n\tcapital: {blue(info.get('capital', ''))} {info.get('base_currency', '')} ({info.get('commissions', '')})"
|
|
102
|
+
_s += f"\n\tinstruments: {blue(smbs)}"
|
|
103
|
+
if params:
|
|
104
|
+
formats = ["{" + f":<{i}" + "}" for i in [50]]
|
|
105
|
+
_p = pd.DataFrame.from_dict(info.get("parameters", {}), orient="index")
|
|
106
|
+
for i in _p.to_string(
|
|
107
|
+
max_colwidth=30,
|
|
108
|
+
header=False,
|
|
109
|
+
formatters=[(lambda x: cyan(fmt.format(str(x)))) for fmt in formats],
|
|
110
|
+
justify="left",
|
|
111
|
+
).split("\n"):
|
|
112
|
+
_s += f"\n\t | {yellow(i)}"
|
|
113
|
+
print(_s)
|
|
114
|
+
|
|
115
|
+
if with_metrics:
|
|
116
|
+
r = TradingSessionResult.from_file(info["path"])
|
|
117
|
+
metric = _pfl_metrics_prepare(r, True, 365)
|
|
118
|
+
_m_repr = str(metric[0][["Gain", "Cagr", "Sharpe", "Max dd pct", "Qr", "Fees"]].round(3)).split("\n")[
|
|
119
|
+
:-1
|
|
120
|
+
]
|
|
121
|
+
for i in _m_repr:
|
|
122
|
+
print("\t " + cyan(i))
|
|
123
|
+
print()
|
|
124
|
+
|
|
125
|
+
def delete(self, name: str | int):
|
|
126
|
+
print(red(f" -> Danger zone - you are about to delete {name} ..."))
|
|
127
|
+
for info in self.results.values():
|
|
128
|
+
match name:
|
|
129
|
+
case int():
|
|
130
|
+
if info.get("idx", -1) == name:
|
|
131
|
+
Path(info["path"]).unlink()
|
|
132
|
+
print(f" -> Deleted {red(name)} ...")
|
|
133
|
+
self.reload()
|
|
134
|
+
return
|
|
135
|
+
case str():
|
|
136
|
+
if info.get("name", "") == name:
|
|
137
|
+
Path(info["path"]).unlink()
|
|
138
|
+
print(f" -> Deleted {red(name)} ...")
|
|
139
|
+
self.reload()
|
|
140
|
+
return
|
|
141
|
+
print(f" -> No results found for {red(name)} !")
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from operator import neg
|
|
3
|
-
from typing import Dict, List
|
|
4
3
|
|
|
5
4
|
import numpy as np
|
|
6
5
|
from sortedcontainers import SortedDict
|
|
@@ -14,8 +13,6 @@ from qubx.core.basics import (
|
|
|
14
13
|
Order,
|
|
15
14
|
OrderSide,
|
|
16
15
|
OrderType,
|
|
17
|
-
Position,
|
|
18
|
-
Signal,
|
|
19
16
|
TransactionCostsCalculator,
|
|
20
17
|
dt_64,
|
|
21
18
|
)
|
|
@@ -36,10 +33,10 @@ class OmeReport:
|
|
|
36
33
|
class OrdersManagementEngine:
|
|
37
34
|
instrument: Instrument
|
|
38
35
|
time_service: ITimeProvider
|
|
39
|
-
active_orders:
|
|
40
|
-
stop_orders:
|
|
41
|
-
asks: SortedDict[float,
|
|
42
|
-
bids: SortedDict[float,
|
|
36
|
+
active_orders: dict[str, Order]
|
|
37
|
+
stop_orders: dict[str, Order]
|
|
38
|
+
asks: SortedDict[float, list[str]]
|
|
39
|
+
bids: SortedDict[float, list[str]]
|
|
43
40
|
bbo: Quote | None # current best bid/ask order book (simplest impl)
|
|
44
41
|
__order_id: int
|
|
45
42
|
__trade_id: int
|
|
@@ -78,10 +75,10 @@ class OrdersManagementEngine:
|
|
|
78
75
|
def get_quote(self) -> Quote:
|
|
79
76
|
return self.bbo
|
|
80
77
|
|
|
81
|
-
def get_open_orders(self) ->
|
|
78
|
+
def get_open_orders(self) -> list[Order]:
|
|
82
79
|
return list(self.active_orders.values()) + list(self.stop_orders.values())
|
|
83
80
|
|
|
84
|
-
def update_bbo(self, quote: Quote) ->
|
|
81
|
+
def update_bbo(self, quote: Quote) -> list[OmeReport]:
|
|
85
82
|
timestamp = self.time_service.time()
|
|
86
83
|
rep = []
|
|
87
84
|
|
|
@@ -151,7 +148,7 @@ class OrdersManagementEngine:
|
|
|
151
148
|
return self._process_order(timestamp, order)
|
|
152
149
|
|
|
153
150
|
def _dbg(self, message, **kwargs) -> None:
|
|
154
|
-
logger.debug(f"[
|
|
151
|
+
logger.debug(f" [<y>OME</y>(<g>{self.instrument}</g>)] :: {message}", **kwargs)
|
|
155
152
|
|
|
156
153
|
def _process_order(self, timestamp: dt_64, order: Order) -> OmeReport:
|
|
157
154
|
if order.status in ["CLOSED", "CANCELED"]:
|
|
@@ -179,7 +176,7 @@ class OrdersManagementEngine:
|
|
|
179
176
|
self.stop_orders[order.id] = order
|
|
180
177
|
|
|
181
178
|
elif order.type == "STOP_LIMIT":
|
|
182
|
-
# TODO: check trigger conditions in options etc
|
|
179
|
+
# TODO: (OME) check trigger conditions in options etc
|
|
183
180
|
raise NotImplementedError("'STOP_LIMIT' order is not supported in Qubx simulator yet !")
|
|
184
181
|
|
|
185
182
|
# - if order must be "executed" immediately
|
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
import re
|
|
2
2
|
from itertools import product
|
|
3
3
|
from types import FunctionType
|
|
4
|
-
from typing import Any,
|
|
4
|
+
from typing import Any, Callable, Type
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
|
|
8
|
+
from qubx.utils.misc import generate_name
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
def _wrap_single_list(param_grid: list | dict) -> dict[str, Any] | list:
|
|
10
12
|
"""
|
|
11
13
|
Wraps all non list values as single
|
|
12
14
|
:param param_grid:
|
|
13
15
|
:return:
|
|
14
16
|
"""
|
|
15
|
-
as_list = lambda x: x if isinstance(x, (tuple, list, dict, np.ndarray)) else [x]
|
|
17
|
+
as_list = lambda x: x if isinstance(x, (tuple, list, dict, np.ndarray)) else [x] # noqa: E731
|
|
16
18
|
if isinstance(param_grid, list):
|
|
17
19
|
return [_wrap_single_list(ps) for ps in param_grid]
|
|
18
20
|
return {k: as_list(v) for k, v in param_grid.items()}
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
def permutate_params(
|
|
22
|
-
parameters:
|
|
23
|
-
conditions: FunctionType |
|
|
24
|
+
parameters: dict[str, list | tuple | Any],
|
|
25
|
+
conditions: FunctionType | list | tuple | None = None,
|
|
24
26
|
wrap_as_list=False,
|
|
25
|
-
) ->
|
|
27
|
+
) -> list[dict]:
|
|
26
28
|
"""
|
|
27
29
|
Generate list of all permutations for given parameters and theirs possible values
|
|
28
30
|
|
|
@@ -115,7 +117,7 @@ def dicts_product(d1: dict, d2: dict) -> dict:
|
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
"""
|
|
118
|
-
flatten = lambda l: [item for sublist in l for item in (sublist if isinstance(sublist, list) else [sublist])]
|
|
120
|
+
flatten = lambda l: [item for sublist in l for item in (sublist if isinstance(sublist, list) else [sublist])] # noqa: E731
|
|
119
121
|
return {(a + " + " + b): flatten([d1[a], d2[b]]) for a, b in product(d1.keys(), d2.keys())}
|
|
120
122
|
|
|
121
123
|
|
|
@@ -124,7 +126,7 @@ class _dict(dict):
|
|
|
124
126
|
return _dict(dicts_product(self, other))
|
|
125
127
|
|
|
126
128
|
|
|
127
|
-
def variate(clz: Type[Any] |
|
|
129
|
+
def variate(clz: Type[Any] | list[Type[Any]], *args, conditions=None, **kwargs) -> _dict:
|
|
128
130
|
"""
|
|
129
131
|
Make variations of parameters for simulations (micro optimizer)
|
|
130
132
|
|
|
@@ -169,19 +171,24 @@ def variate(clz: Type[Any] | List[Type[Any]], *args, conditions=None, **kwargs)
|
|
|
169
171
|
def _cmprss(xs: str):
|
|
170
172
|
return "".join([x[0] for x in re.split(r"((?<!-)(?=[A-Z]))|_|(\d)", xs) if x])
|
|
171
173
|
|
|
172
|
-
if isinstance(clz, type):
|
|
174
|
+
if isinstance(clz, (type, Callable)):
|
|
173
175
|
sfx = _cmprss(clz.__name__)
|
|
174
|
-
_mk = lambda k, *args, **kwargs: k(*args, **kwargs)
|
|
176
|
+
_mk = lambda k, *args, **kwargs: k(*args, **kwargs) # noqa: E731
|
|
175
177
|
elif isinstance(clz, (list, tuple)) and clz and isinstance(clz[0], type):
|
|
176
178
|
sfx = _cmprss(clz[0].__name__)
|
|
177
|
-
_mk = lambda k, *args, **kwargs: [k[0](*args, **kwargs), *k[1:]]
|
|
179
|
+
_mk = lambda k, *args, **kwargs: [k[0](*args, **kwargs), *k[1:]] # noqa: E731
|
|
178
180
|
else:
|
|
179
181
|
raise ValueError(
|
|
180
182
|
"Can't recognize data for variating: must be either a class type or a list where first element is class type"
|
|
181
183
|
)
|
|
182
184
|
|
|
185
|
+
def _v_to_str(x: Any) -> str:
|
|
186
|
+
if isinstance(x, (list, tuple, dict, set, np.ndarray)) and len(xs := str(x)) > 15:
|
|
187
|
+
return "[" + generate_name(xs, 8).lower() + "]"
|
|
188
|
+
return str(x)
|
|
189
|
+
|
|
183
190
|
to_excl = [s for s, v in kwargs.items() if not isinstance(v, (list, set, tuple, range))]
|
|
184
|
-
dic2str = lambda ds: [_cmprss(k) + "=" +
|
|
191
|
+
dic2str = lambda ds: [_cmprss(k) + "=" + _v_to_str(v) for k, v in ds.items() if k not in to_excl] # noqa: E731
|
|
185
192
|
|
|
186
193
|
return _dict(
|
|
187
194
|
{
|