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.

Files changed (101) hide show
  1. {qubx-0.5.5 → qubx-0.5.7}/PKG-INFO +12 -9
  2. {qubx-0.5.5 → qubx-0.5.7}/README.md +7 -3
  3. {qubx-0.5.5 → qubx-0.5.7}/build.py +3 -7
  4. {qubx-0.5.5 → qubx-0.5.7}/pyproject.toml +16 -7
  5. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/_nb_magic.py +16 -16
  6. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/backtester/__init__.py +2 -0
  7. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/backtester/account.py +4 -3
  8. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/backtester/data.py +4 -0
  9. qubx-0.5.7/src/qubx/backtester/management.py +378 -0
  10. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/backtester/ome.py +3 -3
  11. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/backtester/optimization.py +5 -2
  12. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/backtester/simulated_data.py +1 -1
  13. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/connectors/ccxt/account.py +0 -1
  14. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/connectors/ccxt/broker.py +3 -1
  15. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/connectors/ccxt/factory.py +0 -1
  16. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/connectors/ccxt/utils.py +4 -10
  17. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/account.py +1 -1
  18. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/basics.py +1 -1
  19. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/context.py +6 -2
  20. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/helpers.py +29 -15
  21. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/interfaces.py +29 -3
  22. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/loggers.py +1 -2
  23. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/lookups.py +57 -31
  24. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/metrics.py +40 -2
  25. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/mixins/__init__.py +8 -0
  26. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/mixins/processing.py +21 -5
  27. qubx-0.5.7/src/qubx/core/mixins/universe.py +270 -0
  28. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/data/__init__.py +18 -6
  29. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/data/helpers.py +20 -11
  30. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/data/readers.py +28 -25
  31. qubx-0.5.7/src/qubx/math/__init__.py +3 -0
  32. qubx-0.5.7/src/qubx/pandaz/__init__.py +23 -0
  33. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/pandaz/ta.py +192 -21
  34. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/trackers/riskctrl.py +14 -2
  35. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/trackers/sizers.py +7 -1
  36. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/runner/runner.py +12 -1
  37. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/time.py +85 -0
  38. qubx-0.5.5/src/qubx/backtester/management.py +0 -141
  39. qubx-0.5.5/src/qubx/core/mixins/universe.py +0 -155
  40. qubx-0.5.5/src/qubx/math/__init__.py +0 -1
  41. qubx-0.5.5/src/qubx/pandaz/__init__.py +0 -15
  42. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/__init__.py +0 -0
  43. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/backtester/broker.py +0 -0
  44. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/backtester/simulator.py +0 -0
  45. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/backtester/utils.py +0 -0
  46. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/cli/__init__.py +0 -0
  47. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/cli/commands.py +0 -0
  48. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/connectors/ccxt/__init__.py +0 -0
  49. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/connectors/ccxt/customizations.py +0 -0
  50. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/connectors/ccxt/data.py +0 -0
  51. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  52. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/__init__.py +0 -0
  53. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/exceptions.py +0 -0
  54. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/mixins/market.py +0 -0
  55. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/mixins/subscription.py +0 -0
  56. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/mixins/trading.py +0 -0
  57. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/series.pxd +0 -0
  58. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/series.pyi +0 -0
  59. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/series.pyx +0 -0
  60. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/utils.pyi +0 -0
  61. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/core/utils.pyx +0 -0
  62. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/data/tardis.py +0 -0
  63. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/gathering/simplest.py +0 -0
  64. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/math/stats.py +0 -0
  65. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/pandaz/utils.py +0 -0
  66. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/resources/instruments/symbols-binance.cm.json +0 -0
  67. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/resources/instruments/symbols-binance.json +0 -0
  68. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/resources/instruments/symbols-binance.um.json +0 -0
  69. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/resources/instruments/symbols-bitfinex.f.json +0 -0
  70. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/resources/instruments/symbols-bitfinex.json +0 -0
  71. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/resources/instruments/symbols-kraken.f.json +0 -0
  72. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/resources/instruments/symbols-kraken.json +0 -0
  73. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/ta/__init__.py +0 -0
  74. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/ta/indicators.pxd +0 -0
  75. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/ta/indicators.pyi +0 -0
  76. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/ta/indicators.pyx +0 -0
  77. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/trackers/__init__.py +0 -0
  78. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/trackers/abvanced.py +0 -0
  79. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/trackers/composite.py +0 -0
  80. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/trackers/rebalancers.py +0 -0
  81. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/__init__.py +0 -0
  82. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/_pyxreloader.py +0 -0
  83. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/charting/lookinglass.py +0 -0
  84. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  85. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/marketdata/binance.py +0 -0
  86. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/marketdata/ccxt.py +0 -0
  87. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/marketdata/dukas.py +0 -0
  88. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/misc.py +0 -0
  89. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/ntp.py +0 -0
  90. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/numbers_utils.py +0 -0
  91. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/orderbook.py +0 -0
  92. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/plotting/__init__.py +0 -0
  93. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/plotting/dashboard.py +0 -0
  94. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/plotting/data.py +0 -0
  95. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/plotting/interfaces.py +0 -0
  96. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  97. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  98. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/runner/__init__.py +0 -0
  99. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
  100. {qubx-0.5.5 → qubx-0.5.7}/src/qubx/utils/runner/accounts.py +0 -0
  101. {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
1
+ Metadata-Version: 2.3
2
2
  Name: Qubx
3
- Version: 0.5.5
4
- Summary: Qubx - quantitative trading framework
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@gmail.com
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/dmarienko/Qubx
45
+ Project-URL: Repository, https://github.com/xLydianSoftware/Qubx
46
46
  Description-Content-Type: text/markdown
47
47
 
48
48
  # Qubx
49
49
 
50
+ [![CI](https://github.com/xLydianSoftware/Qubx/actions/workflows/ci.yml/badge.svg?branch=main)](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
+ [![CI](https://github.com/xLydianSoftware/Qubx/actions/workflows/ci.yml/badge.svg?branch=main)](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
- import os
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.5"
4
- description = "Qubx - quantitative trading framework"
3
+ version = "0.5.7"
4
+ description = "Qubx - Quantitative Trading Framework"
5
5
  authors = [
6
- "Dmitry Marienko <dmitry@gmail.com>",
7
- "Yuriy Arabskyy <yuriy.arabskyy@gmail.com>",
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/dmarienko/Qubx"
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 -s"
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()
@@ -1,3 +1,5 @@
1
+ __all__ = ["BacktestsResultsManager", "variate", "simulate"]
2
+
1
3
  from .management import BacktestsResultsManager
2
4
  from .optimization import variate
3
5
  from .simulator import simulate
@@ -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) -> list[Order]:
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
- return [o for ome in self.ome.values() for o in ome.get_open_orders()]
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