Qubx 0.5.5__tar.gz → 0.5.7__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.5 → qubx-0.5.7}/PKG-INFO +12 -9
- {qubx-0.5.5 → qubx-0.5.7}/README.md +7 -3
- {qubx-0.5.5 → qubx-0.5.7}/build.py +3 -7
- {qubx-0.5.5 → qubx-0.5.7}/pyproject.toml +16 -7
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/_nb_magic.py +16 -16
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/backtester/__init__.py +2 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/backtester/account.py +4 -3
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/backtester/data.py +4 -0
- qubx-0.5.7/src/qubx/backtester/management.py +378 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/backtester/ome.py +3 -3
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/backtester/optimization.py +5 -2
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/backtester/simulated_data.py +1 -1
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/connectors/ccxt/account.py +0 -1
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/connectors/ccxt/broker.py +3 -1
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/connectors/ccxt/factory.py +0 -1
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/connectors/ccxt/utils.py +4 -10
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/account.py +1 -1
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/basics.py +1 -1
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/context.py +6 -2
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/helpers.py +29 -15
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/interfaces.py +29 -3
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/loggers.py +1 -2
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/lookups.py +57 -31
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/metrics.py +40 -2
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/mixins/__init__.py +8 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/mixins/processing.py +21 -5
- qubx-0.5.7/src/qubx/core/mixins/universe.py +270 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/data/__init__.py +18 -6
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/data/helpers.py +20 -11
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/data/readers.py +28 -25
- qubx-0.5.7/src/qubx/math/__init__.py +3 -0
- qubx-0.5.7/src/qubx/pandaz/__init__.py +23 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/pandaz/ta.py +192 -21
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/trackers/riskctrl.py +14 -2
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/trackers/sizers.py +7 -1
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/runner/runner.py +12 -1
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/time.py +85 -0
- qubx-0.5.5/src/qubx/backtester/management.py +0 -141
- qubx-0.5.5/src/qubx/core/mixins/universe.py +0 -155
- qubx-0.5.5/src/qubx/math/__init__.py +0 -1
- qubx-0.5.5/src/qubx/pandaz/__init__.py +0 -15
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/__init__.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/backtester/broker.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/backtester/simulator.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/backtester/utils.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/cli/__init__.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/cli/commands.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/connectors/ccxt/customizations.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/connectors/ccxt/data.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/__init__.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/exceptions.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/mixins/market.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/mixins/subscription.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/mixins/trading.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/series.pxd +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/series.pyi +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/series.pyx +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/utils.pyi +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/utils.pyx +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/data/tardis.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/gathering/simplest.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/math/stats.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/pandaz/utils.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/resources/instruments/symbols-binance.cm.json +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/resources/instruments/symbols-binance.json +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/resources/instruments/symbols-binance.um.json +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/resources/instruments/symbols-bitfinex.f.json +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/resources/instruments/symbols-bitfinex.json +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/resources/instruments/symbols-kraken.f.json +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/resources/instruments/symbols-kraken.json +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/ta/__init__.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/trackers/__init__.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/trackers/abvanced.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/trackers/composite.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/__init__.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/misc.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/ntp.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/orderbook.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/runner/configs.py +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: Qubx
|
|
3
|
-
Version: 0.5.
|
|
4
|
-
Summary: Qubx -
|
|
5
|
-
Home-page: https://github.com/dmarienko/Qubx
|
|
3
|
+
Version: 0.5.7
|
|
4
|
+
Summary: Qubx - Quantitative Trading Framework
|
|
6
5
|
Author: Dmitry Marienko
|
|
7
|
-
Author-email: dmitry@
|
|
6
|
+
Author-email: dmitry.marienko@xlydian.com
|
|
8
7
|
Requires-Python: >=3.10,<4.0
|
|
9
8
|
Classifier: Programming Language :: Python :: 3
|
|
10
9
|
Classifier: Programming Language :: Python :: 3.10
|
|
11
10
|
Classifier: Programming Language :: Python :: 3.11
|
|
12
11
|
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
13
|
Requires-Dist: ccxt (>=4.2.68,<5.0.0)
|
|
14
14
|
Requires-Dist: croniter (>=2.0.5,<3.0.0)
|
|
15
15
|
Requires-Dist: cython (==3.0.8)
|
|
@@ -42,20 +42,25 @@ Requires-Dist: statsmodels (>=0.14.2,<0.15.0)
|
|
|
42
42
|
Requires-Dist: tabulate (>=0.9.0,<0.10.0)
|
|
43
43
|
Requires-Dist: toml (>=0.10.2,<0.11.0)
|
|
44
44
|
Requires-Dist: tqdm
|
|
45
|
-
Project-URL: Repository, https://github.com/
|
|
45
|
+
Project-URL: Repository, https://github.com/xLydianSoftware/Qubx
|
|
46
46
|
Description-Content-Type: text/markdown
|
|
47
47
|
|
|
48
48
|
# Qubx
|
|
49
49
|
|
|
50
|
+
[](https://github.com/xLydianSoftware/Qubx/actions/workflows/ci.yml)
|
|
51
|
+
|
|
50
52
|
## Next generation of Qube quantitative backtesting framework (QUBX)
|
|
51
53
|
```
|
|
52
54
|
⠀⠀⡰⡖⠒⠒⢒⢦⠀⠀
|
|
53
55
|
⠀⢠⠃⠈⢆⣀⣎⣀⣱⡀ QUBX | Quantitative Backtesting Environment
|
|
54
56
|
⠀⢳⠒⠒⡞⠚⡄⠀⡰⠁ (c) 2024, by Dmytro Mariienko
|
|
55
57
|
⠀⠀⠱⣜⣀⣀⣈⣦⠃⠀⠀⠀
|
|
56
|
-
|
|
57
58
|
```
|
|
58
59
|
|
|
60
|
+
## Documentation
|
|
61
|
+
|
|
62
|
+
See [Qubx Documentation](https://xlydiansoftware.github.io/Qubx/en/latest/)
|
|
63
|
+
|
|
59
64
|
## Installation
|
|
60
65
|
> pip install qubx
|
|
61
66
|
|
|
@@ -76,7 +81,6 @@ base_currency = USDT
|
|
|
76
81
|
```
|
|
77
82
|
|
|
78
83
|
## Running tests
|
|
79
|
-
|
|
80
84
|
We use `pytest` for running tests. For running unit tests execute
|
|
81
85
|
```
|
|
82
86
|
just test
|
|
@@ -99,4 +103,3 @@ To run the tests simply call
|
|
|
99
103
|
```
|
|
100
104
|
just test-integration
|
|
101
105
|
```
|
|
102
|
-
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
# Qubx
|
|
2
2
|
|
|
3
|
+
[](https://github.com/xLydianSoftware/Qubx/actions/workflows/ci.yml)
|
|
4
|
+
|
|
3
5
|
## Next generation of Qube quantitative backtesting framework (QUBX)
|
|
4
6
|
```
|
|
5
7
|
⠀⠀⡰⡖⠒⠒⢒⢦⠀⠀
|
|
6
8
|
⠀⢠⠃⠈⢆⣀⣎⣀⣱⡀ QUBX | Quantitative Backtesting Environment
|
|
7
9
|
⠀⢳⠒⠒⡞⠚⡄⠀⡰⠁ (c) 2024, by Dmytro Mariienko
|
|
8
10
|
⠀⠀⠱⣜⣀⣀⣈⣦⠃⠀⠀⠀
|
|
9
|
-
|
|
10
11
|
```
|
|
11
12
|
|
|
13
|
+
## Documentation
|
|
14
|
+
|
|
15
|
+
See [Qubx Documentation](https://xlydiansoftware.github.io/Qubx/en/latest/)
|
|
16
|
+
|
|
12
17
|
## Installation
|
|
13
18
|
> pip install qubx
|
|
14
19
|
|
|
@@ -29,7 +34,6 @@ base_currency = USDT
|
|
|
29
34
|
```
|
|
30
35
|
|
|
31
36
|
## Running tests
|
|
32
|
-
|
|
33
37
|
We use `pytest` for running tests. For running unit tests execute
|
|
34
38
|
```
|
|
35
39
|
just test
|
|
@@ -51,4 +55,4 @@ BINANCE_FUTURES_SECRET=...
|
|
|
51
55
|
To run the tests simply call
|
|
52
56
|
```
|
|
53
57
|
just test-integration
|
|
54
|
-
```
|
|
58
|
+
```
|
|
@@ -4,18 +4,14 @@ import os
|
|
|
4
4
|
import platform
|
|
5
5
|
import shutil
|
|
6
6
|
import subprocess
|
|
7
|
-
import sysconfig
|
|
8
|
-
|
|
9
7
|
from pathlib import Path
|
|
10
|
-
|
|
8
|
+
|
|
11
9
|
import numpy as np
|
|
12
10
|
import toml
|
|
13
|
-
from Cython.Build import build_ext
|
|
14
|
-
from Cython.Build import cythonize
|
|
11
|
+
from Cython.Build import build_ext, cythonize
|
|
15
12
|
from Cython.Compiler import Options
|
|
16
13
|
from Cython.Compiler.Version import version as cython_compiler_version
|
|
17
|
-
from setuptools import Distribution
|
|
18
|
-
from setuptools import Extension
|
|
14
|
+
from setuptools import Distribution, Extension
|
|
19
15
|
|
|
20
16
|
RED, BLUE, GREEN, YLW, RES = "\033[31m", "\033[36m", "\033[32m", "\033[33m", "\033[0m"
|
|
21
17
|
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "Qubx"
|
|
3
|
-
version = "0.5.
|
|
4
|
-
description = "Qubx -
|
|
3
|
+
version = "0.5.7"
|
|
4
|
+
description = "Qubx - Quantitative Trading Framework"
|
|
5
5
|
authors = [
|
|
6
|
-
"Dmitry Marienko <dmitry@
|
|
7
|
-
"Yuriy Arabskyy <yuriy.arabskyy@
|
|
6
|
+
"Dmitry Marienko <dmitry.marienko@xlydian.com>",
|
|
7
|
+
"Yuriy Arabskyy <yuriy.arabskyy@xlydian.com>",
|
|
8
8
|
]
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
packages = [{ include = "qubx", from = "src" }]
|
|
11
|
-
repository = "https://github.com/
|
|
11
|
+
repository = "https://github.com/xLydianSoftware/Qubx"
|
|
12
12
|
include = [
|
|
13
13
|
# Compiled extensions must be included in the wheel distributions
|
|
14
14
|
{ path = "src/**/*.so", format = "wheel" },
|
|
@@ -61,6 +61,7 @@ ipykernel = "^6.29.4"
|
|
|
61
61
|
iprogress = "^0.4"
|
|
62
62
|
click = "^8.1.7"
|
|
63
63
|
ipywidgets = "^8.1.5"
|
|
64
|
+
ruff = "^0.9.7"
|
|
64
65
|
|
|
65
66
|
[build-system]
|
|
66
67
|
requires = [
|
|
@@ -79,18 +80,26 @@ generate-setup-file = false
|
|
|
79
80
|
[tool.poetry.group.test.dependencies]
|
|
80
81
|
pytest = { extras = ["lazyfixture"], version = "^8.2.0" }
|
|
81
82
|
pytest-asyncio = "^0.24.0"
|
|
82
|
-
pytest-mock = "
|
|
83
|
+
pytest-mock = "^3.12.0"
|
|
84
|
+
pytest-lazy-fixture = "^0.6.3"
|
|
85
|
+
pytest-cov = "^4.1.0"
|
|
83
86
|
|
|
84
87
|
[tool.pytest.ini_options]
|
|
85
88
|
asyncio_mode = "auto"
|
|
86
89
|
asyncio_default_fixture_loop_scope = "function"
|
|
87
90
|
pythonpath = ["src"]
|
|
88
91
|
markers = ["integration: mark a test as an integration test"]
|
|
89
|
-
addopts = "--disable-warnings
|
|
92
|
+
addopts = "--disable-warnings"
|
|
90
93
|
filterwarnings = ["ignore:.*Jupyter is migrating.*:DeprecationWarning"]
|
|
91
94
|
|
|
92
95
|
[tool.ruff]
|
|
93
96
|
line-length = 120
|
|
97
|
+
lint.extend-select = ["I"]
|
|
98
|
+
lint.ignore = [
|
|
99
|
+
"E731",
|
|
100
|
+
"E722",
|
|
101
|
+
"E741",
|
|
102
|
+
] # Ignore lambda assignments, bare except, and ambiguous variable names
|
|
94
103
|
|
|
95
104
|
[tool.ruff.lint.extend-per-file-ignores]
|
|
96
105
|
"*.ipynb" = ["F405", "F401", "E701", "E402", "F403", "E401", "E702"]
|
|
@@ -32,25 +32,25 @@ if runtime_env() in ["notebook", "shell"]:
|
|
|
32
32
|
# - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
|
33
33
|
|
|
34
34
|
# - - - - Common stuff - - - -
|
|
35
|
-
from datetime import time, timedelta
|
|
35
|
+
from datetime import time, timedelta # noqa: F401
|
|
36
36
|
|
|
37
|
-
import numpy as np
|
|
38
|
-
import pandas as pd
|
|
37
|
+
import numpy as np # noqa: F401
|
|
38
|
+
import pandas as pd # noqa: F401
|
|
39
39
|
|
|
40
40
|
# - - - - Charting stuff - - - -
|
|
41
|
-
from matplotlib import pyplot as plt
|
|
42
|
-
from tqdm.auto import tqdm
|
|
41
|
+
from matplotlib import pyplot as plt # noqa: F401
|
|
42
|
+
from tqdm.auto import tqdm # noqa: F401
|
|
43
43
|
|
|
44
44
|
# - - - - TA stuff and indicators - - - -
|
|
45
|
-
import qubx.pandaz.ta as pta
|
|
46
|
-
import qubx.ta.indicators as ta
|
|
47
|
-
from qubx.backtester.optimization import variate
|
|
45
|
+
import qubx.pandaz.ta as pta # noqa: F401
|
|
46
|
+
import qubx.ta.indicators as ta # noqa: F401
|
|
47
|
+
from qubx.backtester.optimization import variate # noqa: F401
|
|
48
48
|
|
|
49
49
|
# - - - - Simulator stuff - - - -
|
|
50
|
-
from qubx.backtester.simulator import simulate
|
|
50
|
+
from qubx.backtester.simulator import simulate # noqa: F401
|
|
51
51
|
|
|
52
52
|
# - - - - Portfolio analysis - - - -
|
|
53
|
-
from qubx.core.metrics import (
|
|
53
|
+
from qubx.core.metrics import ( # noqa: F401
|
|
54
54
|
chart_signals,
|
|
55
55
|
drop_symbols,
|
|
56
56
|
get_symbol_pnls,
|
|
@@ -59,10 +59,10 @@ if runtime_env() in ["notebook", "shell"]:
|
|
|
59
59
|
portfolio_metrics,
|
|
60
60
|
tearsheet,
|
|
61
61
|
)
|
|
62
|
-
from qubx.data.helpers import loader
|
|
62
|
+
from qubx.data.helpers import loader # noqa: F401
|
|
63
63
|
|
|
64
64
|
# - - - - Data reading - - - -
|
|
65
|
-
from qubx.data.readers import (
|
|
65
|
+
from qubx.data.readers import ( # noqa: F401
|
|
66
66
|
AsOhlcvSeries,
|
|
67
67
|
AsPandasFrame,
|
|
68
68
|
AsQuotes,
|
|
@@ -74,7 +74,7 @@ if runtime_env() in ["notebook", "shell"]:
|
|
|
74
74
|
)
|
|
75
75
|
|
|
76
76
|
# - - - - Utils - - - -
|
|
77
|
-
from qubx.pandaz.utils import (
|
|
77
|
+
from qubx.pandaz.utils import ( # noqa: F401
|
|
78
78
|
continuous_periods,
|
|
79
79
|
drop_duplicated_indexes,
|
|
80
80
|
generate_equal_date_ranges,
|
|
@@ -84,9 +84,9 @@ if runtime_env() in ["notebook", "shell"]:
|
|
|
84
84
|
scols,
|
|
85
85
|
srows,
|
|
86
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
|
|
87
|
+
from qubx.utils.charting.lookinglass import LookingGlass # noqa: F401
|
|
88
|
+
from qubx.utils.charting.mpl_helpers import fig, ohlc_plot, plot_trends, sbp, subplot # noqa: F401
|
|
89
|
+
from qubx.utils.misc import this_project_root # noqa: F401
|
|
90
90
|
|
|
91
91
|
# - setup short numpy output format
|
|
92
92
|
np_fmt_short()
|
|
@@ -48,14 +48,15 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
|
|
|
48
48
|
if self._fill_stop_order_at_price:
|
|
49
49
|
logger.info(f"[<y>{self.__class__.__name__}</y>] :: emulates stop orders executions at exact price")
|
|
50
50
|
|
|
51
|
-
def get_orders(self, instrument: Instrument | None = None) ->
|
|
51
|
+
def get_orders(self, instrument: Instrument | None = None) -> dict[str, Order]:
|
|
52
52
|
if instrument is not None:
|
|
53
53
|
ome = self.ome.get(instrument)
|
|
54
54
|
if ome is None:
|
|
55
55
|
raise ValueError(f"ExchangeService:get_orders :: No OME configured for '{instrument}'!")
|
|
56
|
-
return ome.get_open_orders()
|
|
57
56
|
|
|
58
|
-
|
|
57
|
+
return {o.id: o for o in ome.get_open_orders()}
|
|
58
|
+
|
|
59
|
+
return {o.id: o for ome in self.ome.values() for o in ome.get_open_orders()}
|
|
59
60
|
|
|
60
61
|
def get_position(self, instrument: Instrument) -> Position:
|
|
61
62
|
if instrument in self.positions:
|
|
@@ -229,6 +229,10 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
229
229
|
"""
|
|
230
230
|
bars = []
|
|
231
231
|
|
|
232
|
+
# - if no records, return empty list to avoid exception from infer_series_frequency
|
|
233
|
+
if not records:
|
|
234
|
+
return bars
|
|
235
|
+
|
|
232
236
|
_data_tf = infer_series_frequency([r.time for r in records[:50]])
|
|
233
237
|
timeframe_ns = _data_tf.item()
|
|
234
238
|
|
|
@@ -0,0 +1,378 @@
|
|
|
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
|
|
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
|
+
- load_config(name)
|
|
34
|
+
Loads the configuration YAML file for a specific backtest result
|
|
35
|
+
- delete(name)
|
|
36
|
+
Deletes one or more backtest results
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, path: str):
|
|
40
|
+
self.path = path
|
|
41
|
+
self.reload()
|
|
42
|
+
|
|
43
|
+
def reload(self) -> "BacktestsResultsManager":
|
|
44
|
+
self.results = {}
|
|
45
|
+
self.variations = {}
|
|
46
|
+
|
|
47
|
+
_vars = defaultdict(list)
|
|
48
|
+
names = defaultdict(lambda: 0)
|
|
49
|
+
for p in Path(self.path).glob("**/*.zip"):
|
|
50
|
+
with zipfile.ZipFile(p, "r") as zip_ref:
|
|
51
|
+
try:
|
|
52
|
+
info = yaml.safe_load(zip_ref.read("info.yml"))
|
|
53
|
+
info["path"] = str(p)
|
|
54
|
+
n = info.get("name", "")
|
|
55
|
+
var_set_name = info.get("variation_name", "")
|
|
56
|
+
|
|
57
|
+
# - put variations aside
|
|
58
|
+
if var_set_name:
|
|
59
|
+
_vars[var_set_name].append(info)
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
_new_name = n if names[n] == 0 else f"{n}.{names[n]}"
|
|
63
|
+
names[n] += 1
|
|
64
|
+
info["name"] = _new_name
|
|
65
|
+
self.results[_new_name] = info
|
|
66
|
+
except Exception:
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
# - reindex
|
|
70
|
+
_idx = 1
|
|
71
|
+
for n in sorted(self.results.keys()):
|
|
72
|
+
self.results[n]["idx"] = _idx
|
|
73
|
+
_idx += 1
|
|
74
|
+
|
|
75
|
+
# - reindex variations at the end
|
|
76
|
+
for n in sorted(_vars.keys()):
|
|
77
|
+
self.variations[_idx] = {
|
|
78
|
+
"name": n,
|
|
79
|
+
"idx": _idx,
|
|
80
|
+
"variations": _vars[n],
|
|
81
|
+
"created": pd.Timestamp(_vars[n][0].get("creation_time", "")).round("1s"),
|
|
82
|
+
"author": _vars[n][0].get("author", ""),
|
|
83
|
+
"description": _vars[n][0].get("description", ""),
|
|
84
|
+
}
|
|
85
|
+
_idx += 1
|
|
86
|
+
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
def __getitem__(
|
|
90
|
+
self, name: str | int | list[int] | list[str] | slice
|
|
91
|
+
) -> TradingSessionResult | list[TradingSessionResult]:
|
|
92
|
+
return self.load(name)
|
|
93
|
+
|
|
94
|
+
def load(
|
|
95
|
+
self, name_or_idx: str | int | list[int] | list[str] | slice
|
|
96
|
+
) -> TradingSessionResult | list[TradingSessionResult]:
|
|
97
|
+
match name_or_idx:
|
|
98
|
+
case list():
|
|
99
|
+
return [self.load(i) for i in name_or_idx] # type: ignore
|
|
100
|
+
case str():
|
|
101
|
+
return [self.load(i) for i in self._find_indices(name_or_idx)] # type: ignore
|
|
102
|
+
case slice():
|
|
103
|
+
return [
|
|
104
|
+
self.load(i)
|
|
105
|
+
for i in range(name_or_idx.start, name_or_idx.stop, name_or_idx.step if name_or_idx.step else 1)
|
|
106
|
+
] # type: ignore
|
|
107
|
+
case int():
|
|
108
|
+
if name_or_idx > len(self.results) and name_or_idx in self.variations:
|
|
109
|
+
return [
|
|
110
|
+
TradingSessionResult.from_file(v.get("path", ""))
|
|
111
|
+
for v in self.variations[name_or_idx].get("variations", [])
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
# - load by index
|
|
115
|
+
for info in self.results.values():
|
|
116
|
+
if info.get("idx", -1) == name_or_idx:
|
|
117
|
+
return TradingSessionResult.from_file(info["path"])
|
|
118
|
+
|
|
119
|
+
raise ValueError(f"No result found for '{name_or_idx}' !")
|
|
120
|
+
|
|
121
|
+
def load_config(self, name: str | int) -> str:
|
|
122
|
+
"""Load the configuration YAML file for a specific backtest result.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
name (str | int): The name or index of the backtest result. If str, matches against the backtest name.
|
|
126
|
+
If int, matches against the backtest index.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
str: The contents of the configuration YAML file as a string.
|
|
130
|
+
|
|
131
|
+
Raises:
|
|
132
|
+
ValueError: If no backtest result is found matching the provided name/index.
|
|
133
|
+
"""
|
|
134
|
+
p = None
|
|
135
|
+
for info in self.results.values():
|
|
136
|
+
match name:
|
|
137
|
+
case int():
|
|
138
|
+
if info.get("idx", -1) == name:
|
|
139
|
+
n = info.get("name", "")
|
|
140
|
+
p = info.get("path", {})
|
|
141
|
+
break
|
|
142
|
+
case str():
|
|
143
|
+
if info.get("name", "") == name:
|
|
144
|
+
n = info.get("name", "")
|
|
145
|
+
p = info.get("path", {})
|
|
146
|
+
break
|
|
147
|
+
if p is None:
|
|
148
|
+
raise ValueError(f"No result found for {name}")
|
|
149
|
+
|
|
150
|
+
# - name may have .1, .2, etc. so we need to remove it
|
|
151
|
+
n = n.split(".")[0] if "." in n else n
|
|
152
|
+
with zipfile.ZipFile(p, "r") as zip_ref:
|
|
153
|
+
return zip_ref.read(f"{n}.yaml").decode("utf-8")
|
|
154
|
+
|
|
155
|
+
def delete(self, name: str | int | list[int] | list[str] | slice):
|
|
156
|
+
"""Delete one or more backtest results.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
name: Identifier(s) for the backtest result(s) to delete. Can be:
|
|
160
|
+
- str: Name of backtest or regex pattern to match multiple backtests
|
|
161
|
+
- int: Index of specific backtest
|
|
162
|
+
- list[int]: List of backtest indices
|
|
163
|
+
- list[str]: List of backtest names
|
|
164
|
+
- slice: Range of backtest indices to delete
|
|
165
|
+
|
|
166
|
+
Prints:
|
|
167
|
+
Message confirming which backtest(s) were deleted, or error if none found.
|
|
168
|
+
Deleted backtest names are shown in red text.
|
|
169
|
+
|
|
170
|
+
Note:
|
|
171
|
+
- For string names, supports regex pattern matching against backtest names and strategy class names
|
|
172
|
+
- Deletes the underlying results files and reloads the results index
|
|
173
|
+
- Operation is irreversible
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
def _del_idx(idx):
|
|
177
|
+
for info in self.results.values():
|
|
178
|
+
if info.get("idx", -1) == idx:
|
|
179
|
+
Path(info["path"]).unlink()
|
|
180
|
+
return info.get("name", idx)
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
match name:
|
|
184
|
+
case str():
|
|
185
|
+
nms = [_del_idx(i) for i in self._find_indices(name)]
|
|
186
|
+
self.reload()
|
|
187
|
+
print(f" -> Deleted {red(', '.join(nms))} ...")
|
|
188
|
+
return
|
|
189
|
+
|
|
190
|
+
case list():
|
|
191
|
+
nms = [_del_idx(i) for i in name]
|
|
192
|
+
self.reload()
|
|
193
|
+
print(f" -> Deleted {red(', '.join(nms))} ...")
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
case slice():
|
|
197
|
+
nms = [_del_idx(i) for i in range(name.start, name.stop, name.step if name.step else 1)]
|
|
198
|
+
self.reload()
|
|
199
|
+
print(f" -> Deleted {red(', '.join(nms))} ...")
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
for info in self.results.values():
|
|
203
|
+
match name:
|
|
204
|
+
case int():
|
|
205
|
+
if info.get("idx", -1) == name:
|
|
206
|
+
Path(info["path"]).unlink()
|
|
207
|
+
print(f" -> Deleted {red(info.get('name', name))} ...")
|
|
208
|
+
self.reload()
|
|
209
|
+
return
|
|
210
|
+
case str():
|
|
211
|
+
if info.get("name", "") == name:
|
|
212
|
+
Path(info["path"]).unlink()
|
|
213
|
+
print(f" -> Deleted {red(info.get('name', name))} ...")
|
|
214
|
+
self.reload()
|
|
215
|
+
return
|
|
216
|
+
print(f" -> No results found for {red(name)} !")
|
|
217
|
+
|
|
218
|
+
def _find_indices(self, regex: str):
|
|
219
|
+
for n in sorted(self.results.keys()):
|
|
220
|
+
info = self.results[n]
|
|
221
|
+
s_cls = info.get("strategy_class", "").split(".")[-1]
|
|
222
|
+
|
|
223
|
+
try:
|
|
224
|
+
if not re.match(regex, n, re.IGNORECASE):
|
|
225
|
+
# if not re.match(regex, s_cls, re.IGNORECASE):
|
|
226
|
+
continue
|
|
227
|
+
except Exception:
|
|
228
|
+
if regex.lower() != n.lower() and regex.lower() != s_cls.lower():
|
|
229
|
+
continue
|
|
230
|
+
|
|
231
|
+
yield info.get("idx", -1)
|
|
232
|
+
|
|
233
|
+
def list(
|
|
234
|
+
self,
|
|
235
|
+
regex: str = "",
|
|
236
|
+
with_metrics=True,
|
|
237
|
+
params=False,
|
|
238
|
+
as_table=False,
|
|
239
|
+
pretty_print=False,
|
|
240
|
+
sort_by: str | None = "sharpe",
|
|
241
|
+
ascending=False,
|
|
242
|
+
show_variations=True,
|
|
243
|
+
):
|
|
244
|
+
"""List backtesting results with optional filtering and formatting.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
- regex (str, optional): Regular expression pattern to filter results by strategy name or class. Defaults to "".
|
|
248
|
+
- with_metrics (bool, optional): Whether to include performance metrics in output. Defaults to True.
|
|
249
|
+
- params (bool, optional): Whether to display strategy parameters. Defaults to False.
|
|
250
|
+
- as_table (bool, optional): Return results as a pandas DataFrame instead of printing. Defaults to False.
|
|
251
|
+
|
|
252
|
+
Returns:
|
|
253
|
+
- Optional[pd.DataFrame]: If as_table=True, returns a DataFrame containing the results sorted by creation time.
|
|
254
|
+
- Otherwise prints formatted results to console.
|
|
255
|
+
"""
|
|
256
|
+
_t_rep = []
|
|
257
|
+
for n in sorted(self.results.keys()):
|
|
258
|
+
info = self.results[n]
|
|
259
|
+
s_cls = info.get("strategy_class", "").split(".")[-1]
|
|
260
|
+
|
|
261
|
+
if regex:
|
|
262
|
+
if not re.match(regex, n, re.IGNORECASE):
|
|
263
|
+
# if not re.match(regex, s_cls, re.IGNORECASE):
|
|
264
|
+
continue
|
|
265
|
+
|
|
266
|
+
name = info.get("name", "")
|
|
267
|
+
smbs = ", ".join(info.get("symbols", list()))
|
|
268
|
+
start = pd.Timestamp(info.get("start", "")).round("1s")
|
|
269
|
+
stop = pd.Timestamp(info.get("stop", "")).round("1s")
|
|
270
|
+
dscr = info.get("description", "")
|
|
271
|
+
created = pd.Timestamp(info.get("creation_time", "")).round("1s")
|
|
272
|
+
metrics = info.get("performance", {})
|
|
273
|
+
author = info.get("author", "")
|
|
274
|
+
_s = f"{yellow(str(info.get('idx')))} - {red(name)} ::: {magenta(created)} by {cyan(author)}"
|
|
275
|
+
|
|
276
|
+
_one_line_dscr = ""
|
|
277
|
+
if dscr:
|
|
278
|
+
dscr = dscr.split("\n")
|
|
279
|
+
for _d in dscr:
|
|
280
|
+
_s += f"\n\t{magenta('# ' + _d)}"
|
|
281
|
+
_one_line_dscr += "\u25cf " + _d + "\n"
|
|
282
|
+
|
|
283
|
+
_s += f"\n\tstrategy: {green(s_cls)}"
|
|
284
|
+
_s += f"\n\tinterval: {blue(start)} - {blue(stop)}"
|
|
285
|
+
_s += f"\n\tcapital: {blue(info.get('capital', ''))} {info.get('base_currency', '')} ({info.get('commissions', '')})"
|
|
286
|
+
_s += f"\n\tinstruments: {blue(smbs)}"
|
|
287
|
+
if params:
|
|
288
|
+
formats = ["{" + f":<{i}" + "}" for i in [50]]
|
|
289
|
+
_p = pd.DataFrame.from_dict(info.get("parameters", {}), orient="index")
|
|
290
|
+
for i in _p.to_string(
|
|
291
|
+
max_colwidth=30,
|
|
292
|
+
header=False,
|
|
293
|
+
formatters=[(lambda x: cyan(fmt.format(str(x)))) for fmt in formats],
|
|
294
|
+
justify="left",
|
|
295
|
+
).split("\n"):
|
|
296
|
+
_s += f"\n\t | {yellow(i)}"
|
|
297
|
+
|
|
298
|
+
if not as_table:
|
|
299
|
+
print(_s)
|
|
300
|
+
|
|
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
|
+
if not as_table:
|
|
310
|
+
print("\t " + red(_h))
|
|
311
|
+
print("\t " + cyan(_v))
|
|
312
|
+
|
|
313
|
+
if not as_table:
|
|
314
|
+
print()
|
|
315
|
+
else:
|
|
316
|
+
metrics = {
|
|
317
|
+
m: round(v, 3)
|
|
318
|
+
for m, v in metrics.items()
|
|
319
|
+
if m in ["gain", "cagr", "sharpe", "qr", "max_dd_pct", "mdd_usd", "fees", "execs"]
|
|
320
|
+
}
|
|
321
|
+
_t_rep.append(
|
|
322
|
+
{"Index": info.get("idx", ""), "Strategy": name}
|
|
323
|
+
| metrics
|
|
324
|
+
| {
|
|
325
|
+
"start": start,
|
|
326
|
+
"stop": stop,
|
|
327
|
+
"Created": created,
|
|
328
|
+
"Author": author,
|
|
329
|
+
"Description": _one_line_dscr,
|
|
330
|
+
},
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
# - variations (only if not as_table for the time being)
|
|
334
|
+
if not as_table and show_variations:
|
|
335
|
+
for _i, vi in self.variations.items():
|
|
336
|
+
n = vi.get("name", "")
|
|
337
|
+
if regex:
|
|
338
|
+
if not re.match(regex, n, re.IGNORECASE):
|
|
339
|
+
continue
|
|
340
|
+
|
|
341
|
+
_s = f"{yellow(str(_i))} - {red(str(n))} set of {len(vi.get('variations'))} variations ::: {magenta(vi.get('created'))} by {cyan(vi.get('author'))}"
|
|
342
|
+
|
|
343
|
+
dscr = vi.get("description", "").split("\n")
|
|
344
|
+
for _d in dscr:
|
|
345
|
+
_s += f"\n\t{magenta('# ' + _d)}"
|
|
346
|
+
|
|
347
|
+
_mtrx = {}
|
|
348
|
+
for v in vi.get("variations", []):
|
|
349
|
+
_nm = v.get("name", "")
|
|
350
|
+
_nm = _nm.split("_")[-1].strip("()")
|
|
351
|
+
_mtrx[_nm] = v.get("performance", {})
|
|
352
|
+
|
|
353
|
+
_m_repr = pd.DataFrame.from_dict(_mtrx, orient="index")[
|
|
354
|
+
["gain", "cagr", "sharpe", "qr", "max_dd_pct", "mdd_usd", "fees", "execs"]
|
|
355
|
+
].astype(float)
|
|
356
|
+
_m_repr = _m_repr.round(3)
|
|
357
|
+
_m_repr = _m_repr.sort_values(by=sort_by, ascending=ascending) if sort_by else _m_repr
|
|
358
|
+
_m_repr = _m_repr.to_string(index=True)
|
|
359
|
+
|
|
360
|
+
print(_s)
|
|
361
|
+
for _i, _l in enumerate(_m_repr.split("\n")):
|
|
362
|
+
if _i == 0:
|
|
363
|
+
print("\t " + red(_l))
|
|
364
|
+
else:
|
|
365
|
+
print("\t " + blue(_l))
|
|
366
|
+
|
|
367
|
+
if as_table:
|
|
368
|
+
_df = pd.DataFrame.from_records(_t_rep, index="Index")
|
|
369
|
+
_df = _df.sort_values(by=sort_by, ascending=ascending) if sort_by else _df
|
|
370
|
+
if pretty_print:
|
|
371
|
+
from IPython.display import HTML
|
|
372
|
+
|
|
373
|
+
return HTML(
|
|
374
|
+
_df.to_html()
|
|
375
|
+
.replace("\\n", "<br><hr style='border-color: #005000; '/>")
|
|
376
|
+
.replace("<td>", '<td align="left" valign="top">')
|
|
377
|
+
)
|
|
378
|
+
return _df
|