Qubx 0.4.3__tar.gz → 0.5.1__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 (97) hide show
  1. {qubx-0.4.3 → qubx-0.5.1}/PKG-INFO +31 -2
  2. {qubx-0.4.3 → qubx-0.5.1}/README.md +25 -0
  3. {qubx-0.4.3 → qubx-0.5.1}/pyproject.toml +37 -15
  4. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/__init__.py +19 -12
  5. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/_nb_magic.py +24 -25
  6. qubx-0.5.1/src/qubx/backtester/account.py +146 -0
  7. qubx-0.5.1/src/qubx/backtester/broker.py +87 -0
  8. qubx-0.5.1/src/qubx/backtester/data.py +272 -0
  9. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/backtester/ome.py +5 -8
  10. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/backtester/optimization.py +5 -5
  11. qubx-0.5.1/src/qubx/backtester/simulated_data.py +516 -0
  12. qubx-0.5.1/src/qubx/backtester/simulator.py +351 -0
  13. qubx-0.5.1/src/qubx/backtester/utils.py +691 -0
  14. qubx-0.5.1/src/qubx/connectors/ccxt/account.py +496 -0
  15. qubx-0.5.1/src/qubx/connectors/ccxt/broker.py +130 -0
  16. qubx-0.4.3/src/qubx/connectors/ccxt/ccxt_customizations.py → qubx-0.5.1/src/qubx/connectors/ccxt/customizations.py +48 -1
  17. qubx-0.5.1/src/qubx/connectors/ccxt/data.py +612 -0
  18. qubx-0.4.3/src/qubx/connectors/ccxt/ccxt_exceptions.py → qubx-0.5.1/src/qubx/connectors/ccxt/exceptions.py +8 -0
  19. qubx-0.5.1/src/qubx/connectors/ccxt/factory.py +94 -0
  20. qubx-0.4.3/src/qubx/connectors/ccxt/ccxt_utils.py → qubx-0.5.1/src/qubx/connectors/ccxt/utils.py +152 -14
  21. qubx-0.5.1/src/qubx/core/account.py +249 -0
  22. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/core/basics.py +317 -179
  23. qubx-0.5.1/src/qubx/core/context.py +412 -0
  24. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/core/exceptions.py +8 -0
  25. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/core/helpers.py +75 -20
  26. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/core/interfaces.py +492 -189
  27. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/core/loggers.py +45 -16
  28. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/core/lookups.py +82 -140
  29. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/core/metrics.py +256 -23
  30. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/core/mixins/__init__.py +1 -1
  31. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/core/mixins/market.py +34 -24
  32. qubx-0.5.1/src/qubx/core/mixins/processing.py +414 -0
  33. qubx-0.5.1/src/qubx/core/mixins/subscription.py +203 -0
  34. qubx-0.5.1/src/qubx/core/mixins/trading.py +87 -0
  35. qubx-0.5.1/src/qubx/core/mixins/universe.py +158 -0
  36. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/core/utils.pyx +1 -1
  37. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/data/helpers.py +33 -31
  38. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/data/readers.py +490 -184
  39. qubx-0.5.1/src/qubx/data/tardis.py +100 -0
  40. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/pandaz/utils.py +27 -6
  41. qubx-0.5.1/src/qubx/plotting/__init__.py +0 -0
  42. qubx-0.5.1/src/qubx/plotting/dashboard.py +151 -0
  43. qubx-0.5.1/src/qubx/plotting/data.py +137 -0
  44. qubx-0.5.1/src/qubx/plotting/interfaces.py +25 -0
  45. qubx-0.5.1/src/qubx/plotting/renderers/__init__.py +0 -0
  46. qubx-0.5.1/src/qubx/plotting/renderers/plotly.py +0 -0
  47. qubx-0.5.1/src/qubx/ta/__init__.py +0 -0
  48. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/trackers/composite.py +5 -5
  49. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/trackers/rebalancers.py +13 -27
  50. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/trackers/riskctrl.py +10 -9
  51. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/trackers/sizers.py +4 -7
  52. qubx-0.5.1/src/qubx/utils/_jupyter_runner.pyt +59 -0
  53. qubx-0.5.1/src/qubx/utils/marketdata/ccxt.py +88 -0
  54. qubx-0.5.1/src/qubx/utils/marketdata/dukas.py +130 -0
  55. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/utils/misc.py +91 -5
  56. qubx-0.5.1/src/qubx/utils/numbers_utils.py +7 -0
  57. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/utils/orderbook.py +15 -21
  58. qubx-0.5.1/src/qubx/utils/runner.py +543 -0
  59. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/utils/time.py +62 -15
  60. qubx-0.4.3/src/qubx/backtester/queue.py +0 -250
  61. qubx-0.4.3/src/qubx/backtester/simulator.py +0 -1076
  62. qubx-0.4.3/src/qubx/connectors/ccxt/ccxt_connector.py +0 -676
  63. qubx-0.4.3/src/qubx/connectors/ccxt/ccxt_trading.py +0 -242
  64. qubx-0.4.3/src/qubx/core/account.py +0 -240
  65. qubx-0.4.3/src/qubx/core/context.py +0 -289
  66. qubx-0.4.3/src/qubx/core/mixins/processing.py +0 -389
  67. qubx-0.4.3/src/qubx/core/mixins/subscription.py +0 -78
  68. qubx-0.4.3/src/qubx/core/mixins/trading.py +0 -73
  69. qubx-0.4.3/src/qubx/core/mixins/universe.py +0 -140
  70. qubx-0.4.3/src/qubx/utils/collections.py +0 -53
  71. qubx-0.4.3/src/qubx/utils/runner.py +0 -384
  72. qubx-0.4.3/src/qubx/utils/threading.py +0 -14
  73. {qubx-0.4.3 → qubx-0.5.1}/build.py +0 -0
  74. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/backtester/__init__.py +0 -0
  75. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/connectors/ccxt/__init__.py +0 -0
  76. /qubx-0.4.3/src/qubx/core/__init__.py → /qubx-0.5.1/src/qubx/connectors/ccxt/ccxt_connector.py +0 -0
  77. {qubx-0.4.3/src/qubx/ta → qubx-0.5.1/src/qubx/core}/__init__.py +0 -0
  78. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/core/series.pxd +0 -0
  79. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/core/series.pyi +0 -0
  80. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/core/series.pyx +0 -0
  81. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/core/utils.pyi +0 -0
  82. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/data/__init__.py +0 -0
  83. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/gathering/simplest.py +0 -0
  84. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/math/__init__.py +0 -0
  85. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/math/stats.py +0 -0
  86. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/pandaz/__init__.py +0 -0
  87. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/pandaz/ta.py +0 -0
  88. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/ta/indicators.pxd +0 -0
  89. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/ta/indicators.pyi +0 -0
  90. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/ta/indicators.pyx +0 -0
  91. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/trackers/__init__.py +0 -0
  92. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/utils/__init__.py +0 -0
  93. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/utils/_pyxreloader.py +0 -0
  94. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/utils/charting/lookinglass.py +0 -0
  95. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  96. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/utils/marketdata/binance.py +0 -0
  97. {qubx-0.4.3 → qubx-0.5.1}/src/qubx/utils/ntp.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: Qubx
3
- Version: 0.4.3
3
+ Version: 0.5.1
4
4
  Summary: Qubx - quantitative trading framework
5
5
  Home-page: https://github.com/dmarienko/Qubx
6
6
  Author: Dmitry Marienko
@@ -13,7 +13,10 @@ Classifier: Programming Language :: Python :: 3.12
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)
16
+ Requires-Dist: dash (>=2.18.2,<3.0.0)
17
+ Requires-Dist: dash-bootstrap-components (>=1.6.0,<2.0.0)
16
18
  Requires-Dist: importlib-metadata
19
+ Requires-Dist: jupyter-console (>=6.6.3,<7.0.0)
17
20
  Requires-Dist: loguru (>=0.7.2,<0.8.0)
18
21
  Requires-Dist: matplotlib (>=3.8.4,<4.0.0)
19
22
  Requires-Dist: msgspec (>=0.18.6,<0.19.0)
@@ -28,14 +31,15 @@ Requires-Dist: psycopg-pool (>=3.2.2,<4.0.0)
28
31
  Requires-Dist: pyarrow (>=15.0.0,<16.0.0)
29
32
  Requires-Dist: pydantic (>=2.9.2,<3.0.0)
30
33
  Requires-Dist: pymongo (>=4.6.1,<5.0.0)
31
- Requires-Dist: pytest[lazyfixture] (>=7.2.0,<8.0.0)
32
34
  Requires-Dist: python-binance (>=1.0.19,<2.0.0)
33
35
  Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
36
+ Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
34
37
  Requires-Dist: scikit-learn (>=1.4.2,<2.0.0)
35
38
  Requires-Dist: scipy (>=1.12.0,<2.0.0)
36
39
  Requires-Dist: sortedcontainers (>=2.4.0,<3.0.0)
37
40
  Requires-Dist: stackprinter (>=0.2.10,<0.3.0)
38
41
  Requires-Dist: statsmodels (>=0.14.2,<0.15.0)
42
+ Requires-Dist: tabulate (>=0.9.0,<0.10.0)
39
43
  Requires-Dist: tqdm
40
44
  Project-URL: Repository, https://github.com/dmarienko/Qubx
41
45
  Description-Content-Type: text/markdown
@@ -70,3 +74,28 @@ base_currency = USDT
70
74
  > python ..\src\qubx\utils\runner.py configs\zero_test.yaml -a configs\.env -j
71
75
  ```
72
76
 
77
+ ## Running tests
78
+
79
+ We use `pytest` for running tests. For running unit tests execute
80
+ ```
81
+ just test
82
+ ```
83
+
84
+ We also have several integration tests (marked with `@pytest.mark.integration`), which mainly make sure that the exchange connectors function properly. We test them on the corresponding testnets, so you will need to generate api credentials for the exchange testnets that you want to verify.
85
+
86
+ Once you have the testnet credentials store them in an `.env.integration` file in the root of the Qubx directory
87
+ ```
88
+ # BINANCE SPOT test credentials
89
+ BINANCE_SPOT_API_KEY=...
90
+ BINANCE_SPOT_SECRET=...
91
+
92
+ # BINANCE FUTURES test credentials
93
+ BINANCE_FUTURES_API_KEY=...
94
+ BINANCE_FUTURES_SECRET=...
95
+ ```
96
+
97
+ To run the tests simply call
98
+ ```
99
+ just test-integration
100
+ ```
101
+
@@ -27,3 +27,28 @@ base_currency = USDT
27
27
  ```
28
28
  > python ..\src\qubx\utils\runner.py configs\zero_test.yaml -a configs\.env -j
29
29
  ```
30
+
31
+ ## Running tests
32
+
33
+ We use `pytest` for running tests. For running unit tests execute
34
+ ```
35
+ just test
36
+ ```
37
+
38
+ We also have several integration tests (marked with `@pytest.mark.integration`), which mainly make sure that the exchange connectors function properly. We test them on the corresponding testnets, so you will need to generate api credentials for the exchange testnets that you want to verify.
39
+
40
+ Once you have the testnet credentials store them in an `.env.integration` file in the root of the Qubx directory
41
+ ```
42
+ # BINANCE SPOT test credentials
43
+ BINANCE_SPOT_API_KEY=...
44
+ BINANCE_SPOT_SECRET=...
45
+
46
+ # BINANCE FUTURES test credentials
47
+ BINANCE_FUTURES_API_KEY=...
48
+ BINANCE_FUTURES_SECRET=...
49
+ ```
50
+
51
+ To run the tests simply call
52
+ ```
53
+ just test-integration
54
+ ```
@@ -1,12 +1,13 @@
1
1
  [tool.poetry]
2
2
  name = "Qubx"
3
- version = "0.4.3"
3
+ version = "0.5.1"
4
4
  description = "Qubx - quantitative trading framework"
5
- authors = ["Dmitry Marienko <dmitry@gmail.com>", "Yuriy Arabskyy <yuriy.arabskyy@gmail.com>"]
6
- readme = "README.md"
7
- packages = [
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
@@ -16,7 +17,6 @@ include = [
16
17
 
17
18
  [tool.poetry.dependencies]
18
19
  python = ">=3.10,<4.0"
19
- pytest = {extras = ["lazyfixture"], version = "^7.2.0"}
20
20
  numpy = "^1.26.3"
21
21
  ntplib = "^0.4.0"
22
22
  loguru = "^0.7.2"
@@ -43,34 +43,56 @@ psycopg-binary = "^3.1.19"
43
43
  psycopg-pool = "^3.2.2"
44
44
  sortedcontainers = "^2.4.0"
45
45
  msgspec = "^0.18.6"
46
+ pyyaml = "^6.0.2"
47
+ dash = "^2.18.2"
48
+ dash-bootstrap-components = "^1.6.0"
49
+ tabulate = "^0.9.0"
50
+ jupyter-console = "^6.6.3"
46
51
 
47
52
  [tool.poetry.group.dev.dependencies]
48
53
  pre-commit = "^2.20.0"
49
- pytest = "^7.1.3"
50
54
  rust-just = "^1.36.0"
51
55
  twine = "^5.1.1"
52
56
 
53
57
  #[project.optional-dependencies]
54
58
  #numba = "^0.57.1"
55
59
  ipykernel = "^6.29.4"
60
+ iprogress = "^0.4"
61
+ click = "^8.1.7"
62
+ ipywidgets = "^8.1.5"
56
63
 
57
64
  [build-system]
58
- requires = ["poetry-core", "setuptools", "numpy>=1.26.3", "cython==3.0.8", "toml>=0.10.2"]
65
+ requires = [
66
+ "poetry-core",
67
+ "setuptools",
68
+ "numpy>=1.26.3",
69
+ "cython==3.0.8",
70
+ "toml>=0.10.2",
71
+ ]
59
72
  build-backend = "poetry.core.masonry.api"
60
73
 
61
74
  [tool.poetry.build]
62
75
  script = "build.py"
63
76
  generate-setup-file = false
64
77
 
65
- [tool.poetry.scripts]
66
- qubx = 'qubx.utils.runner:run'
67
-
68
78
  [tool.poetry.group.test.dependencies]
69
- pytest = "^7.1.3"
79
+ pytest = { extras = ["lazyfixture"], version = "^8.2.0" }
80
+ pytest-asyncio = "^0.24.0"
70
81
  pytest-mock = "*"
71
82
 
72
83
  [tool.pytest.ini_options]
84
+ asyncio_mode = "auto"
85
+ asyncio_default_fixture_loop_scope = "function"
73
86
  pythonpath = ["src"]
74
- filterwarnings = [
75
- "ignore:.*Jupyter is migrating.*:DeprecationWarning",
76
- ]
87
+ markers = ["integration: mark a test as an integration test"]
88
+ addopts = "--disable-warnings -s"
89
+ filterwarnings = ["ignore:.*Jupyter is migrating.*:DeprecationWarning"]
90
+
91
+ [tool.ruff]
92
+ line-length = 120
93
+
94
+ [tool.ruff.lint.extend-per-file-ignores]
95
+ "*.ipynb" = ["F405", "F401", "E701", "E402", "F403", "E401", "E702"]
96
+
97
+ [tool.poetry.scripts]
98
+ qubx = "qubx.utils.runner: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
- import os, sys, stackprinter
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
 
@@ -15,7 +18,10 @@ def formatter(record):
15
18
  if record["level"].name in {"WARNING", "SNAKY"}:
16
19
  fmt = "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - %s" % fmt
17
20
 
18
- prefix = "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> [ <level>%s</level> ] " % record["level"].icon
21
+ prefix = (
22
+ "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> [ <level>%s</level> ] <cyan>({module})</cyan> "
23
+ % record["level"].icon
24
+ )
19
25
 
20
26
  if record["exception"] is not None:
21
27
  # stackprinter.set_excepthook(style='darkbg2')
@@ -29,7 +35,6 @@ def formatter(record):
29
35
 
30
36
 
31
37
  class QubxLogConfig:
32
-
33
38
  @staticmethod
34
39
  def get_log_level():
35
40
  return os.getenv("QUBX_LOG_LEVEL", "DEBUG")
@@ -64,8 +69,8 @@ lookup = GlobalLookup(InstrumentsLookup(), FeesLookup())
64
69
 
65
70
  # registering magic for jupyter notebook
66
71
  if runtime_env() in ["notebook", "shell"]:
67
- from IPython.core.magic import Magics, magics_class, line_magic, line_cell_magic
68
72
  from IPython.core.getipython import get_ipython
73
+ from IPython.core.magic import Magics, line_cell_magic, line_magic, magics_class
69
74
 
70
75
  @magics_class
71
76
  class QubxMagics(Magics):
@@ -107,14 +112,15 @@ if runtime_env() in ["notebook", "shell"]:
107
112
  if line:
108
113
  if "dark" in line.lower():
109
114
  set_mpl_theme("dark")
115
+ # - temporary workaround for vscode - dark theme not applying to ipywidgets in notebook
116
+ # - see https://github.com/microsoft/vscode-jupyter/issues/7161
117
+ if runtime_env() == "notebook":
118
+ _vscode_clr_trick = """from IPython.display import display, HTML; display(HTML("<style> .cell-output-ipywidget-background { background-color: transparent !important; } :root { --jp-widgets-color: var(--vscode-editor-foreground); --jp-widgets-font-size: var(--vscode-editor-font-size); } </style>"))"""
119
+ exec(_vscode_clr_trick, self.shell.user_ns)
110
120
 
111
121
  elif "light" in line.lower():
112
122
  set_mpl_theme("light")
113
123
 
114
- # install additional plotly helpers
115
- # from qube.charting.plot_helpers import install_plotly_helpers
116
- # install_plotly_helpers()
117
-
118
124
  def _get_manager(self):
119
125
  if self.__manager is None:
120
126
  import multiprocessing as m
@@ -133,7 +139,8 @@ if runtime_env() in ["notebook", "shell"]:
133
139
 
134
140
  """
135
141
  import multiprocessing as m
136
- import time, re
142
+ import re
143
+ import time
137
144
 
138
145
  # create ext args
139
146
  name = None
@@ -148,7 +155,7 @@ if runtime_env() in ["notebook", "shell"]:
148
155
  return
149
156
 
150
157
  ipy = get_ipython()
151
- 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]:
152
159
  ipy.push({a: self._get_manager().Value(None, None)})
153
160
 
154
161
  # code to run
@@ -1,4 +1,4 @@
1
- """"
1
+ """ "
2
2
  Here stuff we want to have in every Jupyter notebook after calling %qubx magic
3
3
  """
4
4
 
@@ -27,66 +27,65 @@ def np_fmt_reset():
27
27
 
28
28
 
29
29
  if runtime_env() in ["notebook", "shell"]:
30
-
31
30
  # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
32
31
  # -- all imports below will appear in notebook after calling %%qubx magic ---
33
32
  # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
34
33
 
35
34
  # - - - - Common stuff - - - -
35
+ from datetime import time, timedelta
36
+
36
37
  import numpy as np
37
38
  import pandas as pd
38
- from datetime import time, timedelta
39
+
40
+ # - - - - Charting stuff - - - -
41
+ from matplotlib import pyplot as plt
39
42
  from tqdm.auto import tqdm
40
43
 
41
44
  # - - - - TA stuff and indicators - - - -
42
45
  import qubx.pandaz.ta as pta
43
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
44
51
 
45
52
  # - - - - Portfolio analysis - - - -
46
53
  from qubx.core.metrics import (
47
- tearsheet,
48
54
  chart_signals,
49
- get_symbol_pnls,
50
- get_equity,
51
- portfolio_metrics,
52
- pnl,
53
55
  drop_symbols,
56
+ get_symbol_pnls,
54
57
  pick_symbols,
58
+ pnl,
59
+ portfolio_metrics,
60
+ tearsheet,
55
61
  )
62
+ from qubx.data.helpers import loader
56
63
 
57
64
  # - - - - Data reading - - - -
58
65
  from qubx.data.readers import (
59
- CsvStorageDataReader,
60
- MultiQdbConnector,
61
- QuestDBConnector,
62
66
  AsOhlcvSeries,
63
67
  AsPandasFrame,
64
68
  AsQuotes,
65
69
  AsTimestampedRecords,
70
+ CsvStorageDataReader,
71
+ MultiQdbConnector,
72
+ QuestDBConnector,
66
73
  RestoreTicksFromOHLC,
67
74
  )
68
- from qubx.data.helpers import loader
69
-
70
- # - - - - Simulator stuff - - - -
71
- from qubx.backtester.simulator import simulate
72
- from qubx.backtester.optimization import variate
73
-
74
- # - - - - Charting stuff - - - -
75
- from matplotlib import pyplot as plt
76
- from qubx.utils.charting.mpl_helpers import fig, subplot, sbp, plot_trends, ohlc_plot
77
- from qubx.utils.charting.lookinglass import LookingGlass
78
75
 
79
76
  # - - - - Utils - - - -
80
77
  from qubx.pandaz.utils import (
81
- scols,
82
- srows,
83
- ohlc_resample,
84
78
  continuous_periods,
85
- generate_equal_date_ranges,
86
79
  drop_duplicated_indexes,
80
+ generate_equal_date_ranges,
81
+ ohlc_resample,
87
82
  retain_columns_and_join,
88
83
  rolling_forward_test_split,
84
+ scols,
85
+ srows,
89
86
  )
87
+ from qubx.utils.charting.lookinglass import LookingGlass
88
+ from qubx.utils.charting.mpl_helpers import fig, ohlc_plot, plot_trends, sbp, subplot
90
89
 
91
90
  # - setup short numpy output format
92
91
  np_fmt_short()
@@ -0,0 +1,146 @@
1
+ from qubx import logger
2
+ from qubx.backtester.ome import OrdersManagementEngine
3
+ from qubx.core.account import BasicAccountProcessor
4
+ from qubx.core.basics import (
5
+ ZERO_COSTS,
6
+ BatchEvent,
7
+ CtrlChannel,
8
+ Instrument,
9
+ Order,
10
+ Position,
11
+ Timestamped,
12
+ TransactionCostsCalculator,
13
+ dt_64,
14
+ )
15
+ from qubx.core.interfaces import ITimeProvider
16
+ from qubx.core.series import Bar, OrderBook, Quote, Trade
17
+
18
+
19
+ class SimulatedAccountProcessor(BasicAccountProcessor):
20
+ ome: dict[Instrument, OrdersManagementEngine]
21
+ order_to_instrument: dict[str, Instrument]
22
+
23
+ _channel: CtrlChannel
24
+ _fill_stop_order_at_price: bool
25
+ _half_tick_size: dict[Instrument, float]
26
+
27
+ def __init__(
28
+ self,
29
+ account_id: str,
30
+ channel: CtrlChannel,
31
+ base_currency: str,
32
+ initial_capital: float,
33
+ time_provider: ITimeProvider,
34
+ tcc: TransactionCostsCalculator = ZERO_COSTS,
35
+ accurate_stop_orders_execution: bool = False,
36
+ ) -> None:
37
+ super().__init__(
38
+ account_id=account_id,
39
+ time_provider=time_provider,
40
+ base_currency=base_currency,
41
+ tcc=tcc,
42
+ initial_capital=initial_capital,
43
+ )
44
+ self.ome = {}
45
+ self.order_to_instrument = {}
46
+ self._channel = channel
47
+ self._half_tick_size = {}
48
+ self._fill_stop_order_at_price = accurate_stop_orders_execution
49
+ if self._fill_stop_order_at_price:
50
+ logger.info(f"{self.__class__.__name__} emulates stop orders executions at exact price")
51
+
52
+ def get_orders(self, instrument: Instrument | None = None) -> list[Order]:
53
+ if instrument is not None:
54
+ ome = self.ome.get(instrument)
55
+ if ome is None:
56
+ raise ValueError(f"ExchangeService:get_orders :: No OME configured for '{instrument}'!")
57
+ return ome.get_open_orders()
58
+
59
+ return [o for ome in self.ome.values() for o in ome.get_open_orders()]
60
+
61
+ def get_position(self, instrument: Instrument) -> Position:
62
+ if instrument in self.positions:
63
+ return self.positions[instrument]
64
+
65
+ # - initiolize OME for this instrument
66
+ self.ome[instrument] = OrdersManagementEngine(
67
+ instrument=instrument,
68
+ time_provider=self.time_provider,
69
+ tcc=self._tcc, # type: ignore
70
+ fill_stop_order_at_price=self._fill_stop_order_at_price,
71
+ )
72
+
73
+ # - initiolize empty position
74
+ position = Position(instrument) # type: ignore
75
+ self._half_tick_size[instrument] = instrument.tick_size / 2 # type: ignore
76
+ self.attach_positions(position)
77
+ return self.positions[instrument]
78
+
79
+ def update_position_price(self, time: dt_64, instrument: Instrument, price: float) -> None:
80
+ super().update_position_price(time, instrument, price)
81
+
82
+ # - first we need to update OME with new quote.
83
+ # - if update is not a quote we need 'emulate' it.
84
+ # - actually if SimulatedExchangeService is used in backtesting mode it will recieve only quotes
85
+ # - case when we need that - SimulatedExchangeService is used for paper trading and data provider configured to listen to OHLC or TAS.
86
+ # - probably we need to subscribe to quotes in real data provider in any case and then this emulation won't be needed.
87
+ quote = price if isinstance(price, Quote) else self.emulate_quote_from_data(instrument, time, price)
88
+ if quote is None:
89
+ return
90
+
91
+ # - process new quote
92
+ self._process_new_quote(instrument, quote)
93
+
94
+ def process_order(self, order: Order, update_locked_value: bool = True) -> None:
95
+ _new = order.status == "NEW"
96
+ _open = order.status == "OPEN"
97
+ _cancel = order.status == "CANCELED"
98
+ _closed = order.status == "CLOSED"
99
+ if _new or _open:
100
+ self.order_to_instrument[order.id] = order.instrument
101
+ if (_cancel or _closed) and order.id in self.order_to_instrument:
102
+ self.order_to_instrument.pop(order.id)
103
+ return super().process_order(order, update_locked_value)
104
+
105
+ def emulate_quote_from_data(
106
+ self, instrument: Instrument, timestamp: dt_64, data: float | Timestamped | BatchEvent
107
+ ) -> Quote | None:
108
+ if instrument not in self._half_tick_size:
109
+ _ = self.get_position(instrument)
110
+
111
+ _ts2 = self._half_tick_size[instrument]
112
+ if isinstance(data, Quote):
113
+ return data
114
+
115
+ elif isinstance(data, Trade):
116
+ if data.taker: # type: ignore
117
+ return Quote(timestamp, data.price - _ts2 * 2, data.price, 0, 0) # type: ignore
118
+ else:
119
+ return Quote(timestamp, data.price, data.price + _ts2 * 2, 0, 0) # type: ignore
120
+
121
+ elif isinstance(data, Bar):
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
+
127
+ elif isinstance(data, BatchEvent):
128
+ return self.emulate_quote_from_data(instrument, timestamp, data.data[-1])
129
+
130
+ elif isinstance(data, float):
131
+ return Quote(timestamp, data - _ts2, data + _ts2, 0, 0)
132
+
133
+ else:
134
+ return None
135
+
136
+ def _process_new_quote(self, instrument: Instrument, data: Quote) -> None:
137
+ ome = self.ome.get(instrument)
138
+ if ome is None:
139
+ logger.warning("ExchangeService:update :: No OME configured for '{symbol}' yet !")
140
+ return
141
+ for r in ome.update_bbo(data):
142
+ if r.exec is not None:
143
+ self.order_to_instrument.pop(r.order.id)
144
+ # - process methods will be called from stg context
145
+ self._channel.send((instrument, "order", r.order, False))
146
+ self._channel.send((instrument, "deals", [r.exec], False))
@@ -0,0 +1,87 @@
1
+ from qubx.backtester.ome import OmeReport
2
+ from qubx.core.basics import (
3
+ CtrlChannel,
4
+ Instrument,
5
+ Order,
6
+ )
7
+ from qubx.core.interfaces import IBroker
8
+
9
+ from .account import SimulatedAccountProcessor
10
+
11
+
12
+ class SimulatedBroker(IBroker):
13
+ channel: CtrlChannel
14
+
15
+ _account: SimulatedAccountProcessor
16
+
17
+ def __init__(
18
+ self,
19
+ channel: CtrlChannel,
20
+ account: SimulatedAccountProcessor,
21
+ exchange_id: str = "simulated",
22
+ ) -> None:
23
+ self.channel = channel
24
+ self._account = account
25
+ self._exchange_id = exchange_id
26
+
27
+ @property
28
+ def is_simulated_trading(self) -> bool:
29
+ return True
30
+
31
+ def send_order(
32
+ self,
33
+ instrument: Instrument,
34
+ order_side: str,
35
+ order_type: str,
36
+ amount: float,
37
+ price: float | None = None,
38
+ client_id: str | None = None,
39
+ time_in_force: str = "gtc",
40
+ **options,
41
+ ) -> Order:
42
+ ome = self._account.ome.get(instrument)
43
+ if ome is None:
44
+ raise ValueError(f"ExchangeService:send_order :: No OME configured for '{instrument.symbol}'!")
45
+
46
+ # - try to place order in OME
47
+ report = ome.place_order(
48
+ order_side.upper(), # type: ignore
49
+ order_type.upper(), # type: ignore
50
+ amount,
51
+ price,
52
+ client_id,
53
+ time_in_force,
54
+ **options,
55
+ )
56
+
57
+ self._send_exec_report(instrument, report)
58
+ return report.order
59
+
60
+ def cancel_order(self, order_id: str) -> Order | None:
61
+ instrument = self._account.order_to_instrument.get(order_id)
62
+ if instrument is None:
63
+ raise ValueError(f"ExchangeService:cancel_order :: can't find order with id = '{order_id}'!")
64
+
65
+ ome = self._account.ome.get(instrument)
66
+ if ome is None:
67
+ raise ValueError(f"ExchangeService:send_order :: No OME configured for '{instrument}'!")
68
+
69
+ # - cancel order in OME and remove from the map to free memory
70
+ order_update = ome.cancel_order(order_id)
71
+ self._send_exec_report(instrument, order_update)
72
+
73
+ return order_update.order
74
+
75
+ def cancel_orders(self, instrument: Instrument) -> None:
76
+ raise NotImplementedError("Not implemented yet")
77
+
78
+ def update_order(self, order_id: str, price: float | None = None, amount: float | None = None) -> Order:
79
+ raise NotImplementedError("Not implemented yet")
80
+
81
+ def _send_exec_report(self, instrument: Instrument, report: OmeReport):
82
+ self.channel.send((instrument, "order", report.order, False))
83
+ if report.exec is not None:
84
+ self.channel.send((instrument, "deals", [report.exec], False))
85
+
86
+ def exchange(self) -> str:
87
+ return self._exchange_id.upper()