Qubx 0.4.2__tar.gz → 0.5.0__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 (93) hide show
  1. {qubx-0.4.2 → qubx-0.5.0}/PKG-INFO +29 -2
  2. {qubx-0.4.2 → qubx-0.5.0}/README.md +25 -0
  3. {qubx-0.4.2 → qubx-0.5.0}/pyproject.toml +22 -8
  4. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/__init__.py +9 -6
  5. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/_nb_magic.py +1 -2
  6. qubx-0.5.0/src/qubx/backtester/account.py +134 -0
  7. qubx-0.5.0/src/qubx/backtester/broker.py +82 -0
  8. qubx-0.5.0/src/qubx/backtester/data.py +269 -0
  9. qubx-0.5.0/src/qubx/backtester/simulated_data.py +517 -0
  10. qubx-0.5.0/src/qubx/backtester/simulator.py +349 -0
  11. qubx-0.5.0/src/qubx/backtester/utils.py +691 -0
  12. qubx-0.5.0/src/qubx/connectors/ccxt/account.py +496 -0
  13. qubx-0.5.0/src/qubx/connectors/ccxt/broker.py +124 -0
  14. qubx-0.4.2/src/qubx/connectors/ccxt/ccxt_customizations.py → qubx-0.5.0/src/qubx/connectors/ccxt/customizations.py +48 -1
  15. qubx-0.5.0/src/qubx/connectors/ccxt/data.py +601 -0
  16. qubx-0.4.2/src/qubx/connectors/ccxt/ccxt_exceptions.py → qubx-0.5.0/src/qubx/connectors/ccxt/exceptions.py +8 -0
  17. qubx-0.5.0/src/qubx/connectors/ccxt/factory.py +94 -0
  18. qubx-0.4.2/src/qubx/connectors/ccxt/ccxt_utils.py → qubx-0.5.0/src/qubx/connectors/ccxt/utils.py +152 -14
  19. qubx-0.5.0/src/qubx/core/account.py +249 -0
  20. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/core/basics.py +351 -136
  21. qubx-0.5.0/src/qubx/core/context.py +405 -0
  22. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/core/exceptions.py +8 -0
  23. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/core/helpers.py +73 -20
  24. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/core/interfaces.py +463 -193
  25. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/core/loggers.py +44 -16
  26. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/core/lookups.py +82 -140
  27. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/core/metrics.py +9 -11
  28. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/core/mixins/__init__.py +1 -1
  29. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/core/mixins/market.py +34 -24
  30. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/core/mixins/processing.py +168 -135
  31. qubx-0.5.0/src/qubx/core/mixins/subscription.py +200 -0
  32. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/core/mixins/trading.py +33 -22
  33. qubx-0.5.0/src/qubx/core/mixins/universe.py +158 -0
  34. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/core/utils.pyx +1 -1
  35. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/data/helpers.py +32 -30
  36. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/data/readers.py +486 -184
  37. qubx-0.5.0/src/qubx/data/tardis.py +100 -0
  38. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/pandaz/utils.py +31 -8
  39. qubx-0.5.0/src/qubx/plotting/__init__.py +0 -0
  40. qubx-0.5.0/src/qubx/plotting/dashboard.py +151 -0
  41. qubx-0.5.0/src/qubx/plotting/data.py +137 -0
  42. qubx-0.5.0/src/qubx/plotting/interfaces.py +25 -0
  43. qubx-0.5.0/src/qubx/plotting/renderers/__init__.py +0 -0
  44. qubx-0.5.0/src/qubx/plotting/renderers/plotly.py +0 -0
  45. qubx-0.5.0/src/qubx/ta/__init__.py +0 -0
  46. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/utils/marketdata/binance.py +8 -2
  47. qubx-0.5.0/src/qubx/utils/marketdata/ccxt.py +88 -0
  48. qubx-0.5.0/src/qubx/utils/marketdata/dukas.py +130 -0
  49. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/utils/misc.py +83 -5
  50. qubx-0.5.0/src/qubx/utils/numbers_utils.py +7 -0
  51. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/utils/orderbook.py +15 -21
  52. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/utils/runner.py +200 -99
  53. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/utils/time.py +62 -15
  54. qubx-0.4.2/src/qubx/backtester/queue.py +0 -250
  55. qubx-0.4.2/src/qubx/backtester/simulator.py +0 -1076
  56. qubx-0.4.2/src/qubx/connectors/ccxt/ccxt_connector.py +0 -676
  57. qubx-0.4.2/src/qubx/connectors/ccxt/ccxt_trading.py +0 -242
  58. qubx-0.4.2/src/qubx/core/account.py +0 -240
  59. qubx-0.4.2/src/qubx/core/context.py +0 -289
  60. qubx-0.4.2/src/qubx/core/mixins/subscription.py +0 -78
  61. qubx-0.4.2/src/qubx/core/mixins/universe.py +0 -140
  62. qubx-0.4.2/src/qubx/utils/collections.py +0 -53
  63. {qubx-0.4.2 → qubx-0.5.0}/build.py +0 -0
  64. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/backtester/__init__.py +0 -0
  65. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/backtester/ome.py +0 -0
  66. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/backtester/optimization.py +0 -0
  67. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/connectors/ccxt/__init__.py +0 -0
  68. /qubx-0.4.2/src/qubx/core/__init__.py → /qubx-0.5.0/src/qubx/connectors/ccxt/ccxt_connector.py +0 -0
  69. {qubx-0.4.2/src/qubx/ta → qubx-0.5.0/src/qubx/core}/__init__.py +0 -0
  70. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/core/series.pxd +0 -0
  71. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/core/series.pyi +0 -0
  72. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/core/series.pyx +0 -0
  73. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/core/utils.pyi +0 -0
  74. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/data/__init__.py +0 -0
  75. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/gathering/simplest.py +0 -0
  76. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/math/__init__.py +0 -0
  77. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/math/stats.py +0 -0
  78. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/pandaz/__init__.py +0 -0
  79. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/pandaz/ta.py +0 -0
  80. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/ta/indicators.pxd +0 -0
  81. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/ta/indicators.pyi +0 -0
  82. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/ta/indicators.pyx +0 -0
  83. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/trackers/__init__.py +0 -0
  84. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/trackers/composite.py +0 -0
  85. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/trackers/rebalancers.py +0 -0
  86. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/trackers/riskctrl.py +0 -0
  87. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/trackers/sizers.py +0 -0
  88. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/utils/__init__.py +0 -0
  89. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/utils/_pyxreloader.py +0 -0
  90. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/utils/charting/lookinglass.py +0 -0
  91. {qubx-0.4.2 → qubx-0.5.0}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  92. /qubx-0.4.2/src/qubx/utils/threading.py → /qubx-0.5.0/src/qubx/utils/helpers.py +0 -0
  93. {qubx-0.4.2 → qubx-0.5.0}/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.2
3
+ Version: 0.5.0
4
4
  Summary: Qubx - quantitative trading framework
5
5
  Home-page: https://github.com/dmarienko/Qubx
6
6
  Author: Dmitry Marienko
@@ -13,6 +13,8 @@ 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
17
19
  Requires-Dist: loguru (>=0.7.2,<0.8.0)
18
20
  Requires-Dist: matplotlib (>=3.8.4,<4.0.0)
@@ -28,9 +30,9 @@ Requires-Dist: psycopg-pool (>=3.2.2,<4.0.0)
28
30
  Requires-Dist: pyarrow (>=15.0.0,<16.0.0)
29
31
  Requires-Dist: pydantic (>=2.9.2,<3.0.0)
30
32
  Requires-Dist: pymongo (>=4.6.1,<5.0.0)
31
- Requires-Dist: pytest[lazyfixture] (>=7.2.0,<8.0.0)
32
33
  Requires-Dist: python-binance (>=1.0.19,<2.0.0)
33
34
  Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
35
+ Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
34
36
  Requires-Dist: scikit-learn (>=1.4.2,<2.0.0)
35
37
  Requires-Dist: scipy (>=1.12.0,<2.0.0)
36
38
  Requires-Dist: sortedcontainers (>=2.4.0,<3.0.0)
@@ -70,3 +72,28 @@ base_currency = USDT
70
72
  > python ..\src\qubx\utils\runner.py configs\zero_test.yaml -a configs\.env -j
71
73
  ```
72
74
 
75
+ ## Running tests
76
+
77
+ We use `pytest` for running tests. For running unit tests execute
78
+ ```
79
+ just test
80
+ ```
81
+
82
+ 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.
83
+
84
+ Once you have the testnet credentials store them in an `.env.integration` file in the root of the Qubx directory
85
+ ```
86
+ # BINANCE SPOT test credentials
87
+ BINANCE_SPOT_API_KEY=...
88
+ BINANCE_SPOT_SECRET=...
89
+
90
+ # BINANCE FUTURES test credentials
91
+ BINANCE_FUTURES_API_KEY=...
92
+ BINANCE_FUTURES_SECRET=...
93
+ ```
94
+
95
+ To run the tests simply call
96
+ ```
97
+ just test-integration
98
+ ```
99
+
@@ -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,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "Qubx"
3
- version = "0.4.2"
3
+ version = "0.5.0"
4
4
  description = "Qubx - quantitative trading framework"
5
5
  authors = ["Dmitry Marienko <dmitry@gmail.com>", "Yuriy Arabskyy <yuriy.arabskyy@gmail.com>"]
6
6
  readme = "README.md"
@@ -16,7 +16,6 @@ include = [
16
16
 
17
17
  [tool.poetry.dependencies]
18
18
  python = ">=3.10,<4.0"
19
- pytest = {extras = ["lazyfixture"], version = "^7.2.0"}
20
19
  numpy = "^1.26.3"
21
20
  ntplib = "^0.4.0"
22
21
  loguru = "^0.7.2"
@@ -43,16 +42,21 @@ psycopg-binary = "^3.1.19"
43
42
  psycopg-pool = "^3.2.2"
44
43
  sortedcontainers = "^2.4.0"
45
44
  msgspec = "^0.18.6"
45
+ pyyaml = "^6.0.2"
46
+ dash = "^2.18.2"
47
+ dash-bootstrap-components = "^1.6.0"
46
48
 
47
49
  [tool.poetry.group.dev.dependencies]
48
50
  pre-commit = "^2.20.0"
49
- pytest = "^7.1.3"
50
51
  rust-just = "^1.36.0"
51
52
  twine = "^5.1.1"
52
53
 
53
54
  #[project.optional-dependencies]
54
55
  #numba = "^0.57.1"
55
56
  ipykernel = "^6.29.4"
57
+ iprogress = "^0.4"
58
+ click = "^8.1.7"
59
+ ipywidgets = "^8.1.5"
56
60
 
57
61
  [build-system]
58
62
  requires = ["poetry-core", "setuptools", "numpy>=1.26.3", "cython==3.0.8", "toml>=0.10.2"]
@@ -62,15 +66,25 @@ build-backend = "poetry.core.masonry.api"
62
66
  script = "build.py"
63
67
  generate-setup-file = false
64
68
 
65
- [tool.poetry.scripts]
66
- qubx = 'qubx.utils.runner:run'
67
-
68
69
  [tool.poetry.group.test.dependencies]
69
- pytest = "^7.1.3"
70
+ pytest = {extras = ["lazyfixture"], version = "^8.2.0"}
71
+ pytest-asyncio = "^0.24.0"
70
72
  pytest-mock = "*"
71
73
 
72
74
  [tool.pytest.ini_options]
75
+ asyncio_mode = "auto"
76
+ asyncio_default_fixture_loop_scope = "function"
73
77
  pythonpath = ["src"]
78
+ markers = [
79
+ "integration: mark a test as an integration test",
80
+ ]
81
+ addopts = "--disable-warnings -s"
74
82
  filterwarnings = [
75
83
  "ignore:.*Jupyter is migrating.*:DeprecationWarning",
76
- ]
84
+ ]
85
+
86
+ [tool.ruff]
87
+ line-length = 120
88
+
89
+ [tool.ruff.lint.extend-per-file-ignores]
90
+ "*.ipynb" = ["F405", "F401", "E701", "E402", "F403", "E401", "E702"]
@@ -15,7 +15,10 @@ def formatter(record):
15
15
  if record["level"].name in {"WARNING", "SNAKY"}:
16
16
  fmt = "<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - %s" % fmt
17
17
 
18
- prefix = "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> [ <level>%s</level> ] " % record["level"].icon
18
+ prefix = (
19
+ "<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> [ <level>%s</level> ] <cyan>({module})</cyan> "
20
+ % record["level"].icon
21
+ )
19
22
 
20
23
  if record["exception"] is not None:
21
24
  # stackprinter.set_excepthook(style='darkbg2')
@@ -29,7 +32,6 @@ def formatter(record):
29
32
 
30
33
 
31
34
  class QubxLogConfig:
32
-
33
35
  @staticmethod
34
36
  def get_log_level():
35
37
  return os.getenv("QUBX_LOG_LEVEL", "DEBUG")
@@ -107,14 +109,15 @@ if runtime_env() in ["notebook", "shell"]:
107
109
  if line:
108
110
  if "dark" in line.lower():
109
111
  set_mpl_theme("dark")
112
+ # - temporary workaround for vscode - dark theme not applying to ipywidgets in notebook
113
+ # - see https://github.com/microsoft/vscode-jupyter/issues/7161
114
+ 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>"))"""
116
+ exec(_vscode_clr_trick, self.shell.user_ns)
110
117
 
111
118
  elif "light" in line.lower():
112
119
  set_mpl_theme("light")
113
120
 
114
- # install additional plotly helpers
115
- # from qube.charting.plot_helpers import install_plotly_helpers
116
- # install_plotly_helpers()
117
-
118
121
  def _get_manager(self):
119
122
  if self.__manager is None:
120
123
  import multiprocessing as m
@@ -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,7 +27,6 @@ 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
  # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
@@ -0,0 +1,134 @@
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
+ CtrlChannel,
7
+ Instrument,
8
+ Order,
9
+ Position,
10
+ TransactionCostsCalculator,
11
+ dt_64,
12
+ )
13
+ from qubx.core.interfaces import ITimeProvider
14
+ from qubx.core.series import Bar, Quote, Trade
15
+
16
+
17
+ class SimulatedAccountProcessor(BasicAccountProcessor):
18
+ ome: dict[Instrument, OrdersManagementEngine]
19
+ order_to_instrument: dict[str, Instrument]
20
+
21
+ _channel: CtrlChannel
22
+ _fill_stop_order_at_price: bool
23
+ _half_tick_size: dict[Instrument, float]
24
+
25
+ def __init__(
26
+ self,
27
+ account_id: str,
28
+ channel: CtrlChannel,
29
+ base_currency: str,
30
+ initial_capital: float,
31
+ time_provider: ITimeProvider,
32
+ tcc: TransactionCostsCalculator = ZERO_COSTS,
33
+ accurate_stop_orders_execution: bool = False,
34
+ ) -> None:
35
+ super().__init__(
36
+ account_id=account_id,
37
+ time_provider=time_provider,
38
+ base_currency=base_currency,
39
+ tcc=tcc,
40
+ initial_capital=initial_capital,
41
+ )
42
+ self.ome = {}
43
+ self.order_to_instrument = {}
44
+ self._channel = channel
45
+ self._half_tick_size = {}
46
+ self._fill_stop_order_at_price = accurate_stop_orders_execution
47
+ if self._fill_stop_order_at_price:
48
+ logger.info(f"{self.__class__.__name__} emulates stop orders executions at exact price")
49
+
50
+ def get_orders(self, instrument: Instrument | None = None) -> list[Order]:
51
+ if instrument is not None:
52
+ ome = self.ome.get(instrument)
53
+ if ome is None:
54
+ raise ValueError(f"ExchangeService:get_orders :: No OME configured for '{instrument}'!")
55
+ return ome.get_open_orders()
56
+
57
+ return [o for ome in self.ome.values() for o in ome.get_open_orders()]
58
+
59
+ def get_position(self, instrument: Instrument) -> Position:
60
+ if instrument in self.positions:
61
+ return self.positions[instrument]
62
+
63
+ # - initiolize OME for this instrument
64
+ self.ome[instrument] = OrdersManagementEngine(
65
+ instrument=instrument,
66
+ time_provider=self.time_provider,
67
+ tcc=self._tcc, # type: ignore
68
+ fill_stop_order_at_price=self._fill_stop_order_at_price,
69
+ )
70
+
71
+ # - initiolize empty position
72
+ position = Position(instrument) # type: ignore
73
+ self._half_tick_size[instrument] = instrument.tick_size / 2 # type: ignore
74
+ self.attach_positions(position)
75
+ return self.positions[instrument]
76
+
77
+ def update_position_price(self, time: dt_64, instrument: Instrument, price: float) -> None:
78
+ super().update_position_price(time, instrument, price)
79
+
80
+ # - first we need to update OME with new quote.
81
+ # - if update is not a quote we need 'emulate' it.
82
+ # - actually if SimulatedExchangeService is used in backtesting mode it will recieve only quotes
83
+ # - case when we need that - SimulatedExchangeService is used for paper trading and data provider configured to listen to OHLC or TAS.
84
+ # - probably we need to subscribe to quotes in real data provider in any case and then this emulation won't be needed.
85
+ quote = price if isinstance(price, Quote) else self.emulate_quote_from_data(instrument, time, price)
86
+ if quote is None:
87
+ return
88
+
89
+ # - process new quote
90
+ self._process_new_quote(instrument, quote)
91
+
92
+ def process_order(self, order: Order, update_locked_value: bool = True) -> None:
93
+ _new = order.status == "NEW"
94
+ _open = order.status == "OPEN"
95
+ _cancel = order.status == "CANCELED"
96
+ _closed = order.status == "CLOSED"
97
+ if _new or _open:
98
+ self.order_to_instrument[order.id] = order.instrument
99
+ if (_cancel or _closed) and order.id in self.order_to_instrument:
100
+ self.order_to_instrument.pop(order.id)
101
+ return super().process_order(order, update_locked_value)
102
+
103
+ def emulate_quote_from_data(
104
+ self, instrument: Instrument, timestamp: dt_64, data: float | Trade | Bar
105
+ ) -> Quote | None:
106
+ if instrument not in self._half_tick_size:
107
+ _ = self.get_position(instrument)
108
+
109
+ _ts2 = self._half_tick_size[instrument]
110
+ if isinstance(data, Quote):
111
+ return data
112
+ elif isinstance(data, Trade):
113
+ if data.taker: # type: ignore
114
+ return Quote(timestamp, data.price - _ts2 * 2, data.price, 0, 0) # type: ignore
115
+ else:
116
+ return Quote(timestamp, data.price, data.price + _ts2 * 2, 0, 0) # type: ignore
117
+ elif isinstance(data, Bar):
118
+ return Quote(timestamp, data.close - _ts2, data.close + _ts2, 0, 0) # type: ignore
119
+ elif isinstance(data, float):
120
+ return Quote(timestamp, data - _ts2, data + _ts2, 0, 0)
121
+ else:
122
+ return None
123
+
124
+ def _process_new_quote(self, instrument: Instrument, data: Quote) -> None:
125
+ ome = self.ome.get(instrument)
126
+ if ome is None:
127
+ logger.warning("ExchangeService:update :: No OME configured for '{symbol}' yet !")
128
+ return
129
+ for r in ome.update_bbo(data):
130
+ if r.exec is not None:
131
+ self.order_to_instrument.pop(r.order.id)
132
+ # - process methods will be called from stg context
133
+ self._channel.send((instrument, "order", r.order, False))
134
+ self._channel.send((instrument, "deals", [r.exec], False))
@@ -0,0 +1,82 @@
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
+ ) -> None:
22
+ self.channel = channel
23
+ self._account = account
24
+
25
+ @property
26
+ def is_simulated_trading(self) -> bool:
27
+ return True
28
+
29
+ def send_order(
30
+ self,
31
+ instrument: Instrument,
32
+ order_side: str,
33
+ order_type: str,
34
+ amount: float,
35
+ price: float | None = None,
36
+ client_id: str | None = None,
37
+ time_in_force: str = "gtc",
38
+ **options,
39
+ ) -> Order:
40
+ ome = self._account.ome.get(instrument)
41
+ if ome is None:
42
+ raise ValueError(f"ExchangeService:send_order :: No OME configured for '{instrument.symbol}'!")
43
+
44
+ # - try to place order in OME
45
+ report = ome.place_order(
46
+ order_side.upper(), # type: ignore
47
+ order_type.upper(), # type: ignore
48
+ amount,
49
+ price,
50
+ client_id,
51
+ time_in_force,
52
+ **options,
53
+ )
54
+
55
+ self._send_exec_report(instrument, report)
56
+ return report.order
57
+
58
+ def cancel_order(self, order_id: str) -> Order | None:
59
+ instrument = self._account.order_to_instrument.get(order_id)
60
+ if instrument is None:
61
+ raise ValueError(f"ExchangeService:cancel_order :: can't find order with id = '{order_id}'!")
62
+
63
+ ome = self._account.ome.get(instrument)
64
+ if ome is None:
65
+ raise ValueError(f"ExchangeService:send_order :: No OME configured for '{instrument}'!")
66
+
67
+ # - cancel order in OME and remove from the map to free memory
68
+ order_update = ome.cancel_order(order_id)
69
+ self._send_exec_report(instrument, order_update)
70
+
71
+ return order_update.order
72
+
73
+ def cancel_orders(self, instrument: Instrument) -> None:
74
+ raise NotImplementedError("Not implemented yet")
75
+
76
+ def update_order(self, order_id: str, price: float | None = None, amount: float | None = None) -> Order:
77
+ raise NotImplementedError("Not implemented yet")
78
+
79
+ def _send_exec_report(self, instrument: Instrument, report: OmeReport):
80
+ self.channel.send((instrument, "order", report.order, False))
81
+ if report.exec is not None:
82
+ self.channel.send((instrument, "deals", [report.exec], False))