Qubx 0.5.0__tar.gz → 0.5.3__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.0 → qubx-0.5.3}/PKG-INFO +4 -1
- {qubx-0.5.0 → qubx-0.5.3}/pyproject.toml +23 -14
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/__init__.py +12 -8
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/_nb_magic.py +24 -23
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/backtester/__init__.py +2 -1
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/backtester/account.py +14 -4
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/backtester/broker.py +5 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/backtester/data.py +61 -38
- qubx-0.5.3/src/qubx/backtester/management.py +119 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/backtester/ome.py +12 -18
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/backtester/optimization.py +23 -16
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/backtester/simulated_data.py +126 -85
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/backtester/simulator.py +137 -127
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/backtester/utils.py +105 -16
- qubx-0.5.3/src/qubx/cli/commands.py +67 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/connectors/ccxt/broker.py +10 -4
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/connectors/ccxt/customizations.py +1 -3
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/connectors/ccxt/data.py +11 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/core/account.py +6 -4
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/core/basics.py +55 -124
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/core/context.py +31 -20
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/core/exceptions.py +2 -2
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/core/helpers.py +43 -20
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/core/interfaces.py +77 -10
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/core/loggers.py +2 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/core/lookups.py +98 -17
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/core/metrics.py +283 -27
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/core/mixins/market.py +11 -4
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/core/mixins/processing.py +58 -68
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/core/mixins/subscription.py +21 -18
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/core/mixins/trading.py +6 -2
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/core/mixins/universe.py +2 -5
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/core/series.pyi +1 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/core/series.pyx +13 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/data/helpers.py +1 -1
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/data/readers.py +8 -4
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/gathering/simplest.py +5 -6
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/math/stats.py +29 -6
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/pandaz/ta.py +6 -9
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/pandaz/utils.py +3 -0
- qubx-0.5.3/src/qubx/resources/instruments/symbols-binance.cm.json +1 -0
- qubx-0.5.3/src/qubx/resources/instruments/symbols-binance.json +1 -0
- qubx-0.5.3/src/qubx/resources/instruments/symbols-binance.um.json +1 -0
- qubx-0.5.3/src/qubx/resources/instruments/symbols-bitfinex.f.json +1 -0
- qubx-0.5.3/src/qubx/resources/instruments/symbols-bitfinex.json +1 -0
- qubx-0.5.3/src/qubx/resources/instruments/symbols-kraken.f.json +1 -0
- qubx-0.5.3/src/qubx/resources/instruments/symbols-kraken.json +1 -0
- qubx-0.5.3/src/qubx/trackers/abvanced.py +236 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/trackers/composite.py +6 -6
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/trackers/rebalancers.py +13 -27
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/trackers/riskctrl.py +135 -88
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/trackers/sizers.py +20 -33
- qubx-0.5.3/src/qubx/utils/__init__.py +5 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/utils/charting/lookinglass.py +36 -75
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/utils/charting/mpl_helpers.py +26 -12
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/utils/marketdata/ccxt.py +3 -1
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/utils/misc.py +85 -15
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/utils/orderbook.py +9 -9
- {qubx-0.5.0/src/qubx → qubx-0.5.3/src/qubx/utils}/plotting/dashboard.py +1 -2
- qubx-0.5.3/src/qubx/utils/runner/__init__.py +1 -0
- qubx-0.5.3/src/qubx/utils/runner/_jupyter_runner.pyt +60 -0
- qubx-0.5.3/src/qubx/utils/runner/accounts.py +88 -0
- qubx-0.5.3/src/qubx/utils/runner/configs.py +63 -0
- qubx-0.5.3/src/qubx/utils/runner/runner.py +421 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/utils/time.py +15 -11
- qubx-0.5.0/src/qubx/utils/__init__.py +0 -4
- qubx-0.5.0/src/qubx/utils/helpers.py +0 -14
- qubx-0.5.0/src/qubx/utils/runner.py +0 -485
- {qubx-0.5.0 → qubx-0.5.3}/README.md +0 -0
- {qubx-0.5.0 → qubx-0.5.3}/build.py +0 -0
- {qubx-0.5.0/src/qubx/connectors/ccxt → qubx-0.5.3/src/qubx/cli}/__init__.py +0 -0
- {qubx-0.5.0/src/qubx/core → qubx-0.5.3/src/qubx/connectors/ccxt}/__init__.py +0 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/connectors/ccxt/account.py +0 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/connectors/ccxt/factory.py +0 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/connectors/ccxt/utils.py +0 -0
- {qubx-0.5.0/src/qubx/plotting → qubx-0.5.3/src/qubx/core}/__init__.py +0 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/core/series.pxd +0 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/core/utils.pyi +0 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/core/utils.pyx +0 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/data/__init__.py +0 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/data/tardis.py +0 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/math/__init__.py +0 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-0.5.0/src/qubx/plotting/renderers → qubx-0.5.3/src/qubx/ta}/__init__.py +0 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/trackers/__init__.py +0 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/utils/ntp.py +0 -0
- {qubx-0.5.0 → qubx-0.5.3}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-0.5.0/src/qubx/ta → qubx-0.5.3/src/qubx/utils/plotting}/__init__.py +0 -0
- {qubx-0.5.0/src/qubx → qubx-0.5.3/src/qubx/utils}/plotting/data.py +0 -0
- {qubx-0.5.0/src/qubx → qubx-0.5.3/src/qubx/utils}/plotting/interfaces.py +0 -0
- /qubx-0.5.0/src/qubx/connectors/ccxt/ccxt_connector.py → /qubx-0.5.3/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-0.5.0/src/qubx → qubx-0.5.3/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.3
|
|
4
4
|
Summary: Qubx - quantitative trading framework
|
|
5
5
|
Home-page: https://github.com/dmarienko/Qubx
|
|
6
6
|
Author: Dmitry Marienko
|
|
@@ -16,6 +16,7 @@ Requires-Dist: cython (==3.0.8)
|
|
|
16
16
|
Requires-Dist: dash (>=2.18.2,<3.0.0)
|
|
17
17
|
Requires-Dist: dash-bootstrap-components (>=1.6.0,<2.0.0)
|
|
18
18
|
Requires-Dist: importlib-metadata
|
|
19
|
+
Requires-Dist: jupyter-console (>=6.6.3,<7.0.0)
|
|
19
20
|
Requires-Dist: loguru (>=0.7.2,<0.8.0)
|
|
20
21
|
Requires-Dist: matplotlib (>=3.8.4,<4.0.0)
|
|
21
22
|
Requires-Dist: msgspec (>=0.18.6,<0.19.0)
|
|
@@ -38,6 +39,8 @@ Requires-Dist: scipy (>=1.12.0,<2.0.0)
|
|
|
38
39
|
Requires-Dist: sortedcontainers (>=2.4.0,<3.0.0)
|
|
39
40
|
Requires-Dist: stackprinter (>=0.2.10,<0.3.0)
|
|
40
41
|
Requires-Dist: statsmodels (>=0.14.2,<0.15.0)
|
|
42
|
+
Requires-Dist: tabulate (>=0.9.0,<0.10.0)
|
|
43
|
+
Requires-Dist: toml (>=0.10.2,<0.11.0)
|
|
41
44
|
Requires-Dist: tqdm
|
|
42
45
|
Project-URL: Repository, https://github.com/dmarienko/Qubx
|
|
43
46
|
Description-Content-Type: text/markdown
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "Qubx"
|
|
3
|
-
version = "0.5.
|
|
3
|
+
version = "0.5.3"
|
|
4
4
|
description = "Qubx - quantitative trading framework"
|
|
5
|
-
authors = [
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
{ include = "qubx", from = "src" },
|
|
5
|
+
authors = [
|
|
6
|
+
"Dmitry Marienko <dmitry@gmail.com>",
|
|
7
|
+
"Yuriy Arabskyy <yuriy.arabskyy@gmail.com>",
|
|
9
8
|
]
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
packages = [{ include = "qubx", from = "src" }]
|
|
10
11
|
repository = "https://github.com/dmarienko/Qubx"
|
|
11
12
|
include = [
|
|
12
13
|
# Compiled extensions must be included in the wheel distributions
|
|
@@ -45,6 +46,9 @@ msgspec = "^0.18.6"
|
|
|
45
46
|
pyyaml = "^6.0.2"
|
|
46
47
|
dash = "^2.18.2"
|
|
47
48
|
dash-bootstrap-components = "^1.6.0"
|
|
49
|
+
tabulate = "^0.9.0"
|
|
50
|
+
jupyter-console = "^6.6.3"
|
|
51
|
+
toml = "^0.10.2"
|
|
48
52
|
|
|
49
53
|
[tool.poetry.group.dev.dependencies]
|
|
50
54
|
pre-commit = "^2.20.0"
|
|
@@ -59,7 +63,13 @@ click = "^8.1.7"
|
|
|
59
63
|
ipywidgets = "^8.1.5"
|
|
60
64
|
|
|
61
65
|
[build-system]
|
|
62
|
-
requires = [
|
|
66
|
+
requires = [
|
|
67
|
+
"poetry-core",
|
|
68
|
+
"setuptools",
|
|
69
|
+
"numpy>=1.26.3",
|
|
70
|
+
"cython==3.0.8",
|
|
71
|
+
"toml>=0.10.2",
|
|
72
|
+
]
|
|
63
73
|
build-backend = "poetry.core.masonry.api"
|
|
64
74
|
|
|
65
75
|
[tool.poetry.build]
|
|
@@ -67,7 +77,7 @@ script = "build.py"
|
|
|
67
77
|
generate-setup-file = false
|
|
68
78
|
|
|
69
79
|
[tool.poetry.group.test.dependencies]
|
|
70
|
-
pytest = {extras = ["lazyfixture"], version = "^8.2.0"}
|
|
80
|
+
pytest = { extras = ["lazyfixture"], version = "^8.2.0" }
|
|
71
81
|
pytest-asyncio = "^0.24.0"
|
|
72
82
|
pytest-mock = "*"
|
|
73
83
|
|
|
@@ -75,16 +85,15 @@ pytest-mock = "*"
|
|
|
75
85
|
asyncio_mode = "auto"
|
|
76
86
|
asyncio_default_fixture_loop_scope = "function"
|
|
77
87
|
pythonpath = ["src"]
|
|
78
|
-
markers = [
|
|
79
|
-
"integration: mark a test as an integration test",
|
|
80
|
-
]
|
|
88
|
+
markers = ["integration: mark a test as an integration test"]
|
|
81
89
|
addopts = "--disable-warnings -s"
|
|
82
|
-
filterwarnings = [
|
|
83
|
-
"ignore:.*Jupyter is migrating.*:DeprecationWarning",
|
|
84
|
-
]
|
|
90
|
+
filterwarnings = ["ignore:.*Jupyter is migrating.*:DeprecationWarning"]
|
|
85
91
|
|
|
86
92
|
[tool.ruff]
|
|
87
93
|
line-length = 120
|
|
88
94
|
|
|
89
95
|
[tool.ruff.lint.extend-per-file-ignores]
|
|
90
|
-
"*.ipynb" = ["F405", "F401", "E701", "E402", "F403", "E401", "E702"]
|
|
96
|
+
"*.ipynb" = ["F405", "F401", "E701", "E402", "F403", "E401", "E702"]
|
|
97
|
+
|
|
98
|
+
[tool.poetry.scripts]
|
|
99
|
+
qubx = "qubx.cli.commands:main"
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
1
3
|
from typing import Callable
|
|
2
|
-
from qubx.utils import set_mpl_theme, runtime_env
|
|
3
|
-
from qubx.utils.misc import install_pyx_recompiler_for_dev
|
|
4
4
|
|
|
5
|
+
import stackprinter
|
|
5
6
|
from loguru import logger
|
|
6
|
-
|
|
7
|
+
|
|
7
8
|
from qubx.core.lookups import FeesLookup, GlobalLookup, InstrumentsLookup
|
|
9
|
+
from qubx.utils import runtime_env, set_mpl_theme
|
|
10
|
+
from qubx.utils.misc import install_pyx_recompiler_for_dev
|
|
8
11
|
|
|
9
12
|
# - TODO: import some main methods from packages
|
|
10
13
|
|
|
@@ -34,7 +37,7 @@ def formatter(record):
|
|
|
34
37
|
class QubxLogConfig:
|
|
35
38
|
@staticmethod
|
|
36
39
|
def get_log_level():
|
|
37
|
-
return os.getenv("QUBX_LOG_LEVEL", "
|
|
40
|
+
return os.getenv("QUBX_LOG_LEVEL", "WARNING")
|
|
38
41
|
|
|
39
42
|
@staticmethod
|
|
40
43
|
def set_log_level(level: str):
|
|
@@ -66,8 +69,8 @@ lookup = GlobalLookup(InstrumentsLookup(), FeesLookup())
|
|
|
66
69
|
|
|
67
70
|
# registering magic for jupyter notebook
|
|
68
71
|
if runtime_env() in ["notebook", "shell"]:
|
|
69
|
-
from IPython.core.magic import Magics, magics_class, line_magic, line_cell_magic
|
|
70
72
|
from IPython.core.getipython import get_ipython
|
|
73
|
+
from IPython.core.magic import Magics, line_cell_magic, line_magic, magics_class
|
|
71
74
|
|
|
72
75
|
@magics_class
|
|
73
76
|
class QubxMagics(Magics):
|
|
@@ -112,7 +115,7 @@ if runtime_env() in ["notebook", "shell"]:
|
|
|
112
115
|
# - temporary workaround for vscode - dark theme not applying to ipywidgets in notebook
|
|
113
116
|
# - see https://github.com/microsoft/vscode-jupyter/issues/7161
|
|
114
117
|
if runtime_env() == "notebook":
|
|
115
|
-
_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>"))"""
|
|
116
119
|
exec(_vscode_clr_trick, self.shell.user_ns)
|
|
117
120
|
|
|
118
121
|
elif "light" in line.lower():
|
|
@@ -136,7 +139,8 @@ if runtime_env() in ["notebook", "shell"]:
|
|
|
136
139
|
|
|
137
140
|
"""
|
|
138
141
|
import multiprocessing as m
|
|
139
|
-
import
|
|
142
|
+
import re
|
|
143
|
+
import time
|
|
140
144
|
|
|
141
145
|
# create ext args
|
|
142
146
|
name = None
|
|
@@ -151,7 +155,7 @@ if runtime_env() in ["notebook", "shell"]:
|
|
|
151
155
|
return
|
|
152
156
|
|
|
153
157
|
ipy = get_ipython()
|
|
154
|
-
for a in [x for x in re.split("[\ ,;]", line.strip()) if x]:
|
|
158
|
+
for a in [x for x in re.split(r"[\ ,;]", line.strip()) if x]:
|
|
155
159
|
ipy.push({a: self._get_manager().Value(None, None)})
|
|
156
160
|
|
|
157
161
|
# code to run
|
|
@@ -32,60 +32,61 @@ if runtime_env() in ["notebook", "shell"]:
|
|
|
32
32
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
33
33
|
|
|
34
34
|
# - - - - Common stuff - - - -
|
|
35
|
+
from datetime import time, timedelta
|
|
36
|
+
|
|
35
37
|
import numpy as np
|
|
36
38
|
import pandas as pd
|
|
37
|
-
|
|
39
|
+
|
|
40
|
+
# - - - - Charting stuff - - - -
|
|
41
|
+
from matplotlib import pyplot as plt
|
|
38
42
|
from tqdm.auto import tqdm
|
|
39
43
|
|
|
40
44
|
# - - - - TA stuff and indicators - - - -
|
|
41
45
|
import qubx.pandaz.ta as pta
|
|
42
46
|
import qubx.ta.indicators as ta
|
|
47
|
+
from qubx.backtester.optimization import variate
|
|
48
|
+
|
|
49
|
+
# - - - - Simulator stuff - - - -
|
|
50
|
+
from qubx.backtester.simulator import simulate
|
|
43
51
|
|
|
44
52
|
# - - - - Portfolio analysis - - - -
|
|
45
53
|
from qubx.core.metrics import (
|
|
46
|
-
tearsheet,
|
|
47
54
|
chart_signals,
|
|
48
|
-
get_symbol_pnls,
|
|
49
|
-
get_equity,
|
|
50
|
-
portfolio_metrics,
|
|
51
|
-
pnl,
|
|
52
55
|
drop_symbols,
|
|
56
|
+
get_symbol_pnls,
|
|
53
57
|
pick_symbols,
|
|
58
|
+
pnl,
|
|
59
|
+
portfolio_metrics,
|
|
60
|
+
tearsheet,
|
|
54
61
|
)
|
|
62
|
+
from qubx.data.helpers import loader
|
|
55
63
|
|
|
56
64
|
# - - - - Data reading - - - -
|
|
57
65
|
from qubx.data.readers import (
|
|
58
|
-
CsvStorageDataReader,
|
|
59
|
-
MultiQdbConnector,
|
|
60
|
-
QuestDBConnector,
|
|
61
66
|
AsOhlcvSeries,
|
|
62
67
|
AsPandasFrame,
|
|
63
68
|
AsQuotes,
|
|
64
69
|
AsTimestampedRecords,
|
|
70
|
+
CsvStorageDataReader,
|
|
71
|
+
MultiQdbConnector,
|
|
72
|
+
QuestDBConnector,
|
|
65
73
|
RestoreTicksFromOHLC,
|
|
66
74
|
)
|
|
67
|
-
from qubx.data.helpers import loader
|
|
68
|
-
|
|
69
|
-
# - - - - Simulator stuff - - - -
|
|
70
|
-
from qubx.backtester.simulator import simulate
|
|
71
|
-
from qubx.backtester.optimization import variate
|
|
72
|
-
|
|
73
|
-
# - - - - Charting stuff - - - -
|
|
74
|
-
from matplotlib import pyplot as plt
|
|
75
|
-
from qubx.utils.charting.mpl_helpers import fig, subplot, sbp, plot_trends, ohlc_plot
|
|
76
|
-
from qubx.utils.charting.lookinglass import LookingGlass
|
|
77
75
|
|
|
78
76
|
# - - - - Utils - - - -
|
|
79
77
|
from qubx.pandaz.utils import (
|
|
80
|
-
scols,
|
|
81
|
-
srows,
|
|
82
|
-
ohlc_resample,
|
|
83
78
|
continuous_periods,
|
|
84
|
-
generate_equal_date_ranges,
|
|
85
79
|
drop_duplicated_indexes,
|
|
80
|
+
generate_equal_date_ranges,
|
|
81
|
+
ohlc_resample,
|
|
86
82
|
retain_columns_and_join,
|
|
87
83
|
rolling_forward_test_split,
|
|
84
|
+
scols,
|
|
85
|
+
srows,
|
|
88
86
|
)
|
|
87
|
+
from qubx.utils.charting.lookinglass import LookingGlass
|
|
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()
|
|
@@ -7,11 +7,12 @@ from qubx.core.basics import (
|
|
|
7
7
|
Instrument,
|
|
8
8
|
Order,
|
|
9
9
|
Position,
|
|
10
|
+
Timestamped,
|
|
10
11
|
TransactionCostsCalculator,
|
|
11
12
|
dt_64,
|
|
12
13
|
)
|
|
13
14
|
from qubx.core.interfaces import ITimeProvider
|
|
14
|
-
from qubx.core.series import Bar, Quote, Trade
|
|
15
|
+
from qubx.core.series import Bar, OrderBook, Quote, Trade
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
class SimulatedAccountProcessor(BasicAccountProcessor):
|
|
@@ -45,7 +46,7 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
|
|
|
45
46
|
self._half_tick_size = {}
|
|
46
47
|
self._fill_stop_order_at_price = accurate_stop_orders_execution
|
|
47
48
|
if self._fill_stop_order_at_price:
|
|
48
|
-
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")
|
|
49
50
|
|
|
50
51
|
def get_orders(self, instrument: Instrument | None = None) -> list[Order]:
|
|
51
52
|
if instrument is not None:
|
|
@@ -101,23 +102,32 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
|
|
|
101
102
|
return super().process_order(order, update_locked_value)
|
|
102
103
|
|
|
103
104
|
def emulate_quote_from_data(
|
|
104
|
-
self, instrument: Instrument, timestamp: dt_64, data: float |
|
|
105
|
+
self, instrument: Instrument, timestamp: dt_64, data: float | Timestamped
|
|
105
106
|
) -> Quote | None:
|
|
106
107
|
if instrument not in self._half_tick_size:
|
|
107
108
|
_ = self.get_position(instrument)
|
|
108
109
|
|
|
109
|
-
_ts2 = self._half_tick_size[instrument]
|
|
110
110
|
if isinstance(data, Quote):
|
|
111
111
|
return data
|
|
112
|
+
|
|
112
113
|
elif isinstance(data, Trade):
|
|
114
|
+
_ts2 = self._half_tick_size[instrument]
|
|
113
115
|
if data.taker: # type: ignore
|
|
114
116
|
return Quote(timestamp, data.price - _ts2 * 2, data.price, 0, 0) # type: ignore
|
|
115
117
|
else:
|
|
116
118
|
return Quote(timestamp, data.price, data.price + _ts2 * 2, 0, 0) # type: ignore
|
|
119
|
+
|
|
117
120
|
elif isinstance(data, Bar):
|
|
121
|
+
_ts2 = self._half_tick_size[instrument]
|
|
118
122
|
return Quote(timestamp, data.close - _ts2, data.close + _ts2, 0, 0) # type: ignore
|
|
123
|
+
|
|
124
|
+
elif isinstance(data, OrderBook):
|
|
125
|
+
return data.to_quote()
|
|
126
|
+
|
|
119
127
|
elif isinstance(data, float):
|
|
128
|
+
_ts2 = self._half_tick_size[instrument]
|
|
120
129
|
return Quote(timestamp, data - _ts2, data + _ts2, 0, 0)
|
|
130
|
+
|
|
121
131
|
else:
|
|
122
132
|
return None
|
|
123
133
|
|
|
@@ -18,9 +18,11 @@ class SimulatedBroker(IBroker):
|
|
|
18
18
|
self,
|
|
19
19
|
channel: CtrlChannel,
|
|
20
20
|
account: SimulatedAccountProcessor,
|
|
21
|
+
exchange_id: str = "simulated",
|
|
21
22
|
) -> None:
|
|
22
23
|
self.channel = channel
|
|
23
24
|
self._account = account
|
|
25
|
+
self._exchange_id = exchange_id
|
|
24
26
|
|
|
25
27
|
@property
|
|
26
28
|
def is_simulated_trading(self) -> bool:
|
|
@@ -80,3 +82,6 @@ class SimulatedBroker(IBroker):
|
|
|
80
82
|
self.channel.send((instrument, "order", report.order, False))
|
|
81
83
|
if report.exec is not None:
|
|
82
84
|
self.channel.send((instrument, "deals", [report.exec], False))
|
|
85
|
+
|
|
86
|
+
def exchange(self) -> str:
|
|
87
|
+
return self._exchange_id.upper()
|
|
@@ -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,44 @@ 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)
|
|
264
|
+
|
|
265
|
+
if q := self._account.emulate_quote_from_data(instrument, t, data):
|
|
266
|
+
self._last_quotes[instrument] = q
|
|
241
267
|
|
|
242
|
-
|
|
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()
|
|
290
|
+
|
|
291
|
+
def exchange(self) -> str:
|
|
292
|
+
return self._exchange_id.upper()
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
if dscr:
|
|
94
|
+
_s += f"\n\t{magenta(dscr)}"
|
|
95
|
+
_s += f"\n\tstrategy: {green(s_cls)}"
|
|
96
|
+
_s += f"\n\tinterval: {blue(start)} - {blue(stop)}"
|
|
97
|
+
_s += f"\n\tcapital: {blue(info.get('capital', ''))} {info.get('base_currency', '')} ({info.get('commissions', '')})"
|
|
98
|
+
_s += f"\n\tinstruments: {blue(smbs)}"
|
|
99
|
+
if params:
|
|
100
|
+
formats = ["{" + f":<{i}" + "}" for i in [50]]
|
|
101
|
+
_p = pd.DataFrame.from_dict(info.get("parameters", {}), orient="index")
|
|
102
|
+
for i in _p.to_string(
|
|
103
|
+
max_colwidth=30,
|
|
104
|
+
header=False,
|
|
105
|
+
formatters=[(lambda x: cyan(fmt.format(str(x)))) for fmt in formats],
|
|
106
|
+
justify="left",
|
|
107
|
+
).split("\n"):
|
|
108
|
+
_s += f"\n\t | {yellow(i)}"
|
|
109
|
+
print(_s)
|
|
110
|
+
|
|
111
|
+
if with_metrics:
|
|
112
|
+
r = TradingSessionResult.from_file(info["path"])
|
|
113
|
+
metric = _pfl_metrics_prepare(r, True, 365)
|
|
114
|
+
_m_repr = str(metric[0][["Gain", "Cagr", "Sharpe", "Max dd pct", "Qr", "Fees"]].round(3)).split("\n")[
|
|
115
|
+
:-1
|
|
116
|
+
]
|
|
117
|
+
for i in _m_repr:
|
|
118
|
+
print("\t " + cyan(i))
|
|
119
|
+
print()
|