Qubx 0.6.0__tar.gz → 0.6.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of Qubx might be problematic. Click here for more details.
- {qubx-0.6.0 → qubx-0.6.3}/PKG-INFO +2 -1
- {qubx-0.6.0 → qubx-0.6.3}/pyproject.toml +30 -28
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/__init__.py +1 -4
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/backtester/account.py +25 -10
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/backtester/data.py +1 -1
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/backtester/ome.py +94 -38
- qubx-0.6.3/src/qubx/backtester/runner.py +248 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/backtester/simulated_data.py +15 -2
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/backtester/simulator.py +26 -166
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/backtester/utils.py +48 -3
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/cli/commands.py +8 -4
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/cli/release.py +12 -4
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/connectors/ccxt/account.py +6 -4
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/connectors/ccxt/data.py +8 -5
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/core/account.py +6 -2
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/core/basics.py +27 -5
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/core/context.py +22 -1
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/core/helpers.py +1 -1
- qubx-0.6.3/src/qubx/core/initializer.py +84 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/core/interfaces.py +263 -14
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/core/loggers.py +47 -11
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/core/metrics.py +2 -2
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/core/mixins/processing.py +36 -7
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/core/series.pxd +22 -1
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/core/series.pyi +21 -5
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/core/series.pyx +149 -3
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/core/utils.pyi +3 -1
- qubx-0.6.3/src/qubx/data/composite.py +149 -0
- qubx-0.6.3/src/qubx/data/hft.py +779 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/data/readers.py +171 -7
- qubx-0.6.3/src/qubx/exporters/__init__.py +11 -0
- qubx-0.6.3/src/qubx/exporters/composite.py +83 -0
- qubx-0.6.3/src/qubx/exporters/formatters/__init__.py +12 -0
- qubx-0.6.3/src/qubx/exporters/formatters/base.py +161 -0
- qubx-0.6.3/src/qubx/exporters/formatters/incremental.py +103 -0
- qubx-0.6.3/src/qubx/exporters/formatters/slack.py +183 -0
- qubx-0.6.3/src/qubx/exporters/redis_streams.py +177 -0
- qubx-0.6.3/src/qubx/exporters/slack.py +174 -0
- qubx-0.6.3/src/qubx/features/__init__.py +14 -0
- qubx-0.6.3/src/qubx/features/core.py +250 -0
- qubx-0.6.3/src/qubx/features/orderbook.py +41 -0
- qubx-0.6.3/src/qubx/features/price.py +20 -0
- qubx-0.6.3/src/qubx/features/trades.py +105 -0
- qubx-0.6.3/src/qubx/features/utils.py +10 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/math/stats.py +6 -4
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/pandaz/__init__.py +8 -0
- qubx-0.6.3/src/qubx/restarts/state_resolvers.py +66 -0
- qubx-0.6.3/src/qubx/restarts/time_finders.py +34 -0
- qubx-0.6.3/src/qubx/restorers/__init__.py +36 -0
- qubx-0.6.3/src/qubx/restorers/balance.py +120 -0
- qubx-0.6.3/src/qubx/restorers/factory.py +201 -0
- qubx-0.6.3/src/qubx/restorers/interfaces.py +80 -0
- qubx-0.6.3/src/qubx/restorers/position.py +137 -0
- qubx-0.6.3/src/qubx/restorers/signal.py +159 -0
- qubx-0.6.3/src/qubx/restorers/state.py +110 -0
- qubx-0.6.3/src/qubx/restorers/utils.py +42 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/ta/indicators.pyi +2 -2
- qubx-0.6.3/src/qubx/trackers/__init__.py +30 -0
- qubx-0.6.0/src/qubx/trackers/abvanced.py → qubx-0.6.3/src/qubx/trackers/advanced.py +69 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/trackers/rebalancers.py +5 -7
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/trackers/sizers.py +5 -7
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/utils/marketdata/binance.py +132 -93
- qubx-0.6.3/src/qubx/utils/runner/__init__.py +0 -0
- qubx-0.6.3/src/qubx/utils/runner/_jupyter_runner.pyt +152 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/utils/runner/configs.py +20 -3
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/utils/runner/runner.py +249 -34
- qubx-0.6.0/src/qubx/trackers/__init__.py +0 -3
- qubx-0.6.0/src/qubx/utils/runner/_jupyter_runner.pyt +0 -60
- {qubx-0.6.0 → qubx-0.6.3}/README.md +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/build.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/_nb_magic.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/backtester/__init__.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/backtester/broker.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/backtester/management.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/backtester/optimization.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/cli/__init__.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/cli/deploy.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/cli/misc.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/connectors/ccxt/broker.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/connectors/ccxt/customizations.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/connectors/ccxt/factory.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/connectors/ccxt/utils.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/core/__init__.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/core/exceptions.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/core/lookups.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/core/mixins/market.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/core/mixins/subscription.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/core/mixins/trading.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/core/mixins/universe.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/core/utils.pyx +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/data/__init__.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/data/helpers.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/data/tardis.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/gathering/simplest.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/math/__init__.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/pandaz/ta.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/pandaz/utils.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/resources/instruments/symbols-binance.cm.json +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/resources/instruments/symbols-binance.json +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/resources/instruments/symbols-binance.um.json +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/resources/instruments/symbols-bitfinex.f.json +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/resources/instruments/symbols-bitfinex.json +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/resources/instruments/symbols-kraken.f.json +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/resources/instruments/symbols-kraken.json +0 -0
- {qubx-0.6.0/src/qubx/ta → qubx-0.6.3/src/qubx/restarts}/__init__.py +0 -0
- {qubx-0.6.0/src/qubx/utils/plotting → qubx-0.6.3/src/qubx/ta}/__init__.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/trackers/composite.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/trackers/riskctrl.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/utils/__init__.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/utils/misc.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/utils/ntp.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/utils/orderbook.py +0 -0
- {qubx-0.6.0/src/qubx/utils/plotting/renderers → qubx-0.6.3/src/qubx/utils/plotting}/__init__.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-0.6.0/src/qubx/utils/runner → qubx-0.6.3/src/qubx/utils/plotting/renderers}/__init__.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/utils/time.py +0 -0
- {qubx-0.6.0 → qubx-0.6.3}/src/qubx/utils/version.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: Qubx
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
4
4
|
Summary: Qubx - Quantitative Trading Framework
|
|
5
5
|
Author: Dmitry Marienko
|
|
6
6
|
Author-email: dmitry.marienko@xlydian.com
|
|
@@ -37,6 +37,7 @@ Requires-Dist: pymongo (>=4.6.1,<5.0.0)
|
|
|
37
37
|
Requires-Dist: python-binance (>=1.0.19,<2.0.0)
|
|
38
38
|
Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
|
|
39
39
|
Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
|
|
40
|
+
Requires-Dist: redis (>=5.2.1,<6.0.0)
|
|
40
41
|
Requires-Dist: scikit-learn (>=1.4.2,<2.0.0)
|
|
41
42
|
Requires-Dist: scipy (>=1.12.0,<2.0.0)
|
|
42
43
|
Requires-Dist: sortedcontainers (>=2.4.0,<3.0.0)
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "Qubx"
|
|
7
|
-
version = "0.6.
|
|
7
|
+
version = "0.6.3"
|
|
8
8
|
description = "Qubx - Quantitative Trading Framework"
|
|
9
9
|
authors = [ "Dmitry Marienko <dmitry.marienko@xlydian.com>", "Yuriy Arabskyy <yuriy.arabskyy@xlydian.com>",]
|
|
10
10
|
readme = "README.md"
|
|
@@ -24,50 +24,51 @@ format = "wheel"
|
|
|
24
24
|
[tool.ruff]
|
|
25
25
|
line-length = 120
|
|
26
26
|
|
|
27
|
+
[tool.poetry.build]
|
|
28
|
+
script = "build.py"
|
|
29
|
+
generate-setup-file = false
|
|
30
|
+
|
|
31
|
+
[tool.poetry.scripts]
|
|
32
|
+
qubx = "qubx.cli.commands:main"
|
|
33
|
+
|
|
27
34
|
[tool.poetry.dependencies]
|
|
28
35
|
python = ">=3.10,<4.0"
|
|
29
36
|
numpy = "^1.26.3"
|
|
37
|
+
pandas = "^2.2.2"
|
|
38
|
+
pyarrow = "^15.0.0"
|
|
39
|
+
scipy = "^1.12.0"
|
|
40
|
+
scikit-learn = "^1.4.2"
|
|
41
|
+
statsmodels = "^0.14.2"
|
|
42
|
+
numba = "^0.59.1"
|
|
43
|
+
sortedcontainers = "^2.4.0"
|
|
30
44
|
ntplib = "^0.4.0"
|
|
45
|
+
python-binance = "^1.0.19"
|
|
46
|
+
ccxt = "^4.2.68"
|
|
47
|
+
pymongo = "^4.6.1"
|
|
48
|
+
redis = "^5.2.1"
|
|
49
|
+
psycopg = "^3.1.18"
|
|
50
|
+
psycopg-binary = "^3.1.19"
|
|
51
|
+
psycopg-pool = "^3.2.2"
|
|
52
|
+
matplotlib = "^3.8.4"
|
|
53
|
+
plotly = "^5.22.0"
|
|
54
|
+
dash = "^2.18.2"
|
|
55
|
+
dash-bootstrap-components = "^1.6.0"
|
|
31
56
|
loguru = "^0.7.2"
|
|
32
57
|
tqdm = "*"
|
|
33
58
|
importlib-metadata = "*"
|
|
34
59
|
stackprinter = "^0.2.10"
|
|
35
|
-
pymongo = "^4.6.1"
|
|
36
60
|
pydantic = "^2.9.2"
|
|
37
61
|
python-dotenv = "^1.0.0"
|
|
38
|
-
python-binance = "^1.0.19"
|
|
39
|
-
pyarrow = "^15.0.0"
|
|
40
|
-
scipy = "^1.12.0"
|
|
41
62
|
cython = "3.0.8"
|
|
42
|
-
ccxt = "^4.2.68"
|
|
43
63
|
croniter = "^2.0.5"
|
|
44
|
-
psycopg = "^3.1.18"
|
|
45
|
-
pandas = "^2.2.2"
|
|
46
|
-
statsmodels = "^0.14.2"
|
|
47
|
-
matplotlib = "^3.8.4"
|
|
48
|
-
numba = "^0.59.1"
|
|
49
|
-
scikit-learn = "^1.4.2"
|
|
50
|
-
plotly = "^5.22.0"
|
|
51
|
-
psycopg-binary = "^3.1.19"
|
|
52
|
-
psycopg-pool = "^3.2.2"
|
|
53
|
-
sortedcontainers = "^2.4.0"
|
|
54
64
|
msgspec = "^0.18.6"
|
|
55
65
|
pyyaml = "^6.0.2"
|
|
56
|
-
dash = "^2.18.2"
|
|
57
|
-
dash-bootstrap-components = "^1.6.0"
|
|
58
66
|
tabulate = "^0.9.0"
|
|
59
|
-
jupyter-console = "^6.6.3"
|
|
60
67
|
toml = "^0.10.2"
|
|
61
68
|
gitpython = "^3.1.44"
|
|
62
|
-
ipywidgets = "^8.1.5"
|
|
63
69
|
jupyter = "^1.1.1"
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
script = "build.py"
|
|
67
|
-
generate-setup-file = false
|
|
68
|
-
|
|
69
|
-
[tool.poetry.scripts]
|
|
70
|
-
qubx = "qubx.cli.commands:main"
|
|
70
|
+
jupyter-console = "^6.6.3"
|
|
71
|
+
ipywidgets = "^8.1.5"
|
|
71
72
|
|
|
72
73
|
[tool.ruff.lint]
|
|
73
74
|
extend-select = [ "I",]
|
|
@@ -77,7 +78,7 @@ ignore = [ "E731", "E722", "E741",]
|
|
|
77
78
|
asyncio_mode = "auto"
|
|
78
79
|
asyncio_default_fixture_loop_scope = "function"
|
|
79
80
|
pythonpath = [ "src",]
|
|
80
|
-
markers = [ "integration: mark
|
|
81
|
+
markers = [ "integration: mark test as requiring external services like Redis", "e2e: mark test as requiring external exchange connections and API credentials",]
|
|
81
82
|
addopts = "--disable-warnings"
|
|
82
83
|
filterwarnings = [ "ignore:.*Jupyter is migrating.*:DeprecationWarning",]
|
|
83
84
|
|
|
@@ -104,6 +105,7 @@ jinja2 = "3.1.5"
|
|
|
104
105
|
mike = "2.1.3"
|
|
105
106
|
mkdocs-jupyter = "0.25.1"
|
|
106
107
|
debugpy = "^1.8.12"
|
|
108
|
+
hftbacktest = "^2.2.0"
|
|
107
109
|
|
|
108
110
|
[tool.poetry.group.test.dependencies]
|
|
109
111
|
pytest-asyncio = "^0.24.0"
|
|
@@ -68,10 +68,6 @@ class QubxLogConfig:
|
|
|
68
68
|
def setup_logger(level: str | None = None, custom_formatter: Callable | None = None):
|
|
69
69
|
global logger
|
|
70
70
|
|
|
71
|
-
# First, remove all existing handlers to prevent resource leaks
|
|
72
|
-
# Use a safer approach that doesn't rely on internal attributes
|
|
73
|
-
logger.remove()
|
|
74
|
-
|
|
75
71
|
config = {
|
|
76
72
|
"handlers": [
|
|
77
73
|
{"sink": sys.stdout, "format": "{time} - {message}"},
|
|
@@ -79,6 +75,7 @@ class QubxLogConfig:
|
|
|
79
75
|
"extra": {"user": "someone"},
|
|
80
76
|
}
|
|
81
77
|
logger.configure(**config)
|
|
78
|
+
logger.remove(None)
|
|
82
79
|
|
|
83
80
|
level = level or QubxLogConfig.get_log_level()
|
|
84
81
|
# Add stdout handler with enqueue=True for thread/process safety
|
|
@@ -12,7 +12,8 @@ from qubx.core.basics import (
|
|
|
12
12
|
dt_64,
|
|
13
13
|
)
|
|
14
14
|
from qubx.core.interfaces import ITimeProvider
|
|
15
|
-
from qubx.core.series import Bar, OrderBook, Quote, Trade
|
|
15
|
+
from qubx.core.series import Bar, OrderBook, Quote, Trade, TradeArray
|
|
16
|
+
from qubx.restorers import RestoredState
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
class SimulatedAccountProcessor(BasicAccountProcessor):
|
|
@@ -32,6 +33,7 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
|
|
|
32
33
|
time_provider: ITimeProvider,
|
|
33
34
|
tcc: TransactionCostsCalculator = ZERO_COSTS,
|
|
34
35
|
accurate_stop_orders_execution: bool = False,
|
|
36
|
+
restored_state: RestoredState | None = None,
|
|
35
37
|
) -> None:
|
|
36
38
|
super().__init__(
|
|
37
39
|
account_id=account_id,
|
|
@@ -48,6 +50,12 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
|
|
|
48
50
|
if self._fill_stop_order_at_price:
|
|
49
51
|
logger.info(f"[<y>{self.__class__.__name__}</y>] :: emulates stop orders executions at exact price")
|
|
50
52
|
|
|
53
|
+
if restored_state is not None:
|
|
54
|
+
self._balances.update(restored_state.balances)
|
|
55
|
+
for instrument, position in restored_state.positions.items():
|
|
56
|
+
_pos = self.get_position(instrument)
|
|
57
|
+
_pos.reset_by_position(position)
|
|
58
|
+
|
|
51
59
|
def get_orders(self, instrument: Instrument | None = None) -> dict[str, Order]:
|
|
52
60
|
if instrument is not None:
|
|
53
61
|
ome = self.ome.get(instrument)
|
|
@@ -76,20 +84,27 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
|
|
|
76
84
|
self.attach_positions(position)
|
|
77
85
|
return self.positions[instrument]
|
|
78
86
|
|
|
79
|
-
def update_position_price(self, time: dt_64, instrument: Instrument,
|
|
80
|
-
super().update_position_price(time, instrument,
|
|
87
|
+
def update_position_price(self, time: dt_64, instrument: Instrument, update: float | Timestamped) -> None:
|
|
88
|
+
super().update_position_price(time, instrument, update)
|
|
81
89
|
|
|
82
90
|
# - first we need to update OME with new quote.
|
|
83
91
|
# - if update is not a quote we need 'emulate' it.
|
|
84
92
|
# - actually if SimulatedExchangeService is used in backtesting mode it will recieve only quotes
|
|
85
93
|
# - case when we need that - SimulatedExchangeService is used for paper trading and data provider configured to listen to OHLC or TAS.
|
|
86
94
|
# - probably we need to subscribe to quotes in real data provider in any case and then this emulation won't be needed.
|
|
87
|
-
quote =
|
|
95
|
+
quote = update if isinstance(update, Quote) else self.emulate_quote_from_data(instrument, time, update)
|
|
88
96
|
if quote is None:
|
|
89
97
|
return
|
|
90
98
|
|
|
91
|
-
# - process new
|
|
92
|
-
self.
|
|
99
|
+
# - process new data
|
|
100
|
+
self._process_new_data(instrument, quote)
|
|
101
|
+
|
|
102
|
+
def process_market_data(self, time: dt_64, instrument: Instrument, update: Timestamped) -> None:
|
|
103
|
+
if isinstance(update, (TradeArray, Quote, Trade, OrderBook)):
|
|
104
|
+
# - process new data
|
|
105
|
+
self._process_new_data(instrument, update)
|
|
106
|
+
|
|
107
|
+
super().process_market_data(time, instrument, update)
|
|
93
108
|
|
|
94
109
|
def process_order(self, order: Order, update_locked_value: bool = True) -> None:
|
|
95
110
|
_new = order.status == "NEW"
|
|
@@ -113,7 +128,7 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
|
|
|
113
128
|
|
|
114
129
|
elif isinstance(data, Trade):
|
|
115
130
|
_ts2 = self._half_tick_size[instrument]
|
|
116
|
-
if data.
|
|
131
|
+
if data.side == 1: # type: ignore
|
|
117
132
|
return Quote(timestamp, data.price - _ts2 * 2, data.price, 0, 0) # type: ignore
|
|
118
133
|
else:
|
|
119
134
|
return Quote(timestamp, data.price, data.price + _ts2 * 2, 0, 0) # type: ignore
|
|
@@ -132,12 +147,12 @@ class SimulatedAccountProcessor(BasicAccountProcessor):
|
|
|
132
147
|
else:
|
|
133
148
|
return None
|
|
134
149
|
|
|
135
|
-
def
|
|
150
|
+
def _process_new_data(self, instrument: Instrument, data: Quote | OrderBook | Trade | TradeArray) -> None:
|
|
136
151
|
ome = self.ome.get(instrument)
|
|
137
152
|
if ome is None:
|
|
138
|
-
logger.warning("ExchangeService:update :: No OME configured for '{
|
|
153
|
+
logger.warning(f"ExchangeService:update :: No OME configured for '{instrument}' yet !")
|
|
139
154
|
return
|
|
140
|
-
for r in ome.
|
|
155
|
+
for r in ome.process_market_data(data):
|
|
141
156
|
if r.exec is not None:
|
|
142
157
|
self.order_to_instrument.pop(r.order.id)
|
|
143
158
|
# - process methods will be called from stg context
|
|
@@ -151,7 +151,7 @@ class SimulatedDataProvider(IDataProvider):
|
|
|
151
151
|
self._last_quotes[i] = last_quote
|
|
152
152
|
|
|
153
153
|
# - also need to pass this quote to OME !
|
|
154
|
-
self._account.
|
|
154
|
+
self._account._process_new_data(i, last_quote)
|
|
155
155
|
|
|
156
156
|
logger.debug(f" | subscribed {subscription_type} {i} -> {last_quote}")
|
|
157
157
|
|
|
@@ -19,8 +19,9 @@ from qubx.core.basics import (
|
|
|
19
19
|
from qubx.core.exceptions import (
|
|
20
20
|
ExchangeError,
|
|
21
21
|
InvalidOrder,
|
|
22
|
+
SimulationError,
|
|
22
23
|
)
|
|
23
|
-
from qubx.core.series import Quote
|
|
24
|
+
from qubx.core.series import OrderBook, Quote, Trade, TradeArray
|
|
24
25
|
|
|
25
26
|
|
|
26
27
|
@dataclass
|
|
@@ -31,13 +32,18 @@ class OmeReport:
|
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
class OrdersManagementEngine:
|
|
35
|
+
"""
|
|
36
|
+
Orders Management Engine (OME) is a simple implementation of a management of orders for simulation of a limit order book.
|
|
37
|
+
"""
|
|
38
|
+
|
|
34
39
|
instrument: Instrument
|
|
35
40
|
time_service: ITimeProvider
|
|
36
41
|
active_orders: dict[str, Order]
|
|
37
42
|
stop_orders: dict[str, Order]
|
|
38
43
|
asks: SortedDict[float, list[str]]
|
|
39
44
|
bids: SortedDict[float, list[str]]
|
|
40
|
-
bbo: Quote | None # current best bid/ask order book
|
|
45
|
+
bbo: Quote | None # - current best bid/ask order book
|
|
46
|
+
__prev_bbo: Quote | None # - previous best bid/ask order book
|
|
41
47
|
__order_id: int
|
|
42
48
|
__trade_id: int
|
|
43
49
|
_fill_stops_at_price: bool
|
|
@@ -73,46 +79,80 @@ class OrdersManagementEngine:
|
|
|
73
79
|
return "SIM-EXEC-" + self.instrument.symbol + "-" + str(self.__trade_id)
|
|
74
80
|
|
|
75
81
|
def get_quote(self) -> Quote:
|
|
76
|
-
return self.bbo
|
|
82
|
+
return self.bbo # type: ignore
|
|
77
83
|
|
|
78
84
|
def get_open_orders(self) -> list[Order]:
|
|
79
85
|
return list(self.active_orders.values()) + list(self.stop_orders.values())
|
|
80
86
|
|
|
81
|
-
def
|
|
87
|
+
def process_market_data(self, mdata: Quote | OrderBook | Trade | TradeArray) -> list[OmeReport]:
|
|
88
|
+
"""
|
|
89
|
+
Processes the new market data (quote, trade or trades array) and simulates the execution of pending orders.
|
|
90
|
+
"""
|
|
82
91
|
timestamp = self.time_service.time()
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
self.
|
|
115
|
-
|
|
92
|
+
_exec_report = []
|
|
93
|
+
|
|
94
|
+
# - new quote
|
|
95
|
+
if isinstance(mdata, Quote):
|
|
96
|
+
_b, _a = mdata.bid, mdata.ask
|
|
97
|
+
_bs, _as = _b, _a
|
|
98
|
+
|
|
99
|
+
# - update BBO by new quote
|
|
100
|
+
self.__prev_bbo = self.bbo
|
|
101
|
+
self.bbo = mdata
|
|
102
|
+
|
|
103
|
+
# - bunch of trades
|
|
104
|
+
elif isinstance(mdata, TradeArray):
|
|
105
|
+
_b = mdata.max_buy_price
|
|
106
|
+
_a = mdata.min_sell_price
|
|
107
|
+
_bs, _as = _a, _b
|
|
108
|
+
|
|
109
|
+
# - single trade
|
|
110
|
+
elif isinstance(mdata, Trade):
|
|
111
|
+
_b, _a = mdata.price, mdata.price
|
|
112
|
+
_bs, _as = _b, _a
|
|
113
|
+
|
|
114
|
+
# - order book
|
|
115
|
+
elif isinstance(mdata, OrderBook):
|
|
116
|
+
_b, _a = mdata.top_bid, mdata.top_ask
|
|
117
|
+
_bs, _as = _b, _a
|
|
118
|
+
|
|
119
|
+
else:
|
|
120
|
+
raise SimulationError(f"Invalid market data type: {type(mdata)} for update OME({self.instrument.symbol})")
|
|
121
|
+
|
|
122
|
+
# - when new quote bid is higher than the lowest ask order execute all affected orders
|
|
123
|
+
if self.asks and _b >= self.asks.keys()[0]:
|
|
124
|
+
_asks_to_execute = list(self.asks.irange(0, _b))
|
|
125
|
+
for level in _asks_to_execute:
|
|
126
|
+
for order_id in self.asks[level]:
|
|
127
|
+
order = self.active_orders.pop(order_id)
|
|
128
|
+
_exec_report.append(self._execute_order(timestamp, order.price, order, False))
|
|
129
|
+
self.asks.pop(level)
|
|
130
|
+
|
|
131
|
+
# - when new quote ask is lower than the highest bid order execute all affected orders
|
|
132
|
+
if self.bids and _a <= self.bids.keys()[0]:
|
|
133
|
+
_bids_to_execute = list(self.bids.irange(np.inf, _a))
|
|
134
|
+
for level in _bids_to_execute:
|
|
135
|
+
for order_id in self.bids[level]:
|
|
136
|
+
order = self.active_orders.pop(order_id)
|
|
137
|
+
_exec_report.append(self._execute_order(timestamp, order.price, order, False))
|
|
138
|
+
self.bids.pop(level)
|
|
139
|
+
|
|
140
|
+
# - processing stop orders
|
|
141
|
+
for soid in list(self.stop_orders.keys()):
|
|
142
|
+
so = self.stop_orders[soid]
|
|
143
|
+
_emulate_price_exec = self._fill_stops_at_price or so.options.get(OPTION_FILL_AT_SIGNAL_PRICE, False)
|
|
144
|
+
|
|
145
|
+
if so.side == "BUY" and _as >= so.price:
|
|
146
|
+
_exec_price = _as if not _emulate_price_exec else so.price
|
|
147
|
+
self.stop_orders.pop(soid)
|
|
148
|
+
_exec_report.append(self._execute_order(timestamp, _exec_price, so, True))
|
|
149
|
+
|
|
150
|
+
elif so.side == "SELL" and _bs <= so.price:
|
|
151
|
+
_exec_price = _bs if not _emulate_price_exec else so.price
|
|
152
|
+
self.stop_orders.pop(soid)
|
|
153
|
+
_exec_report.append(self._execute_order(timestamp, _exec_price, so, True))
|
|
154
|
+
|
|
155
|
+
return _exec_report
|
|
116
156
|
|
|
117
157
|
def place_order(
|
|
118
158
|
self,
|
|
@@ -163,7 +203,23 @@ class OrdersManagementEngine:
|
|
|
163
203
|
_need_update_book = False
|
|
164
204
|
|
|
165
205
|
if order.type == "MARKET":
|
|
166
|
-
|
|
206
|
+
if exec_price is None:
|
|
207
|
+
exec_price = c_ask if buy_side else c_bid
|
|
208
|
+
|
|
209
|
+
# - special case - fill at signal price for market order
|
|
210
|
+
# - only for simulation
|
|
211
|
+
# - only if this is valid price: market crossed this desired price on last update
|
|
212
|
+
if order.options.get(OPTION_FILL_AT_SIGNAL_PRICE, False) and order.price and self.__prev_bbo:
|
|
213
|
+
_desired_fill_price = order.price
|
|
214
|
+
|
|
215
|
+
if (buy_side and self.__prev_bbo.ask < _desired_fill_price <= c_ask) or (
|
|
216
|
+
not buy_side and self.__prev_bbo.bid > _desired_fill_price >= c_bid
|
|
217
|
+
):
|
|
218
|
+
exec_price = _desired_fill_price
|
|
219
|
+
else:
|
|
220
|
+
raise SimulationError(
|
|
221
|
+
f"Special execution price at {_desired_fill_price} for market order {order.id} cannot be filled because market didn't cross this price on last update !"
|
|
222
|
+
)
|
|
167
223
|
|
|
168
224
|
elif order.type == "LIMIT":
|
|
169
225
|
_need_update_book = True
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
|
|
6
|
+
from qubx import logger
|
|
7
|
+
from qubx.core.basics import SW, DataType
|
|
8
|
+
from qubx.core.context import StrategyContext
|
|
9
|
+
from qubx.core.exceptions import SimulationConfigError, SimulationError
|
|
10
|
+
from qubx.core.helpers import extract_parameters_from_object, full_qualified_class_name
|
|
11
|
+
from qubx.core.interfaces import IStrategy, IStrategyContext
|
|
12
|
+
from qubx.core.loggers import InMemoryLogsWriter, StrategyLogging
|
|
13
|
+
from qubx.core.lookups import lookup
|
|
14
|
+
from qubx.pandaz.utils import _frame_to_str
|
|
15
|
+
|
|
16
|
+
from .account import SimulatedAccountProcessor
|
|
17
|
+
from .broker import SimulatedBroker
|
|
18
|
+
from .data import SimulatedDataProvider
|
|
19
|
+
from .utils import (
|
|
20
|
+
SetupTypes,
|
|
21
|
+
SignalsProxy,
|
|
22
|
+
SimulatedCtrlChannel,
|
|
23
|
+
SimulatedScheduler,
|
|
24
|
+
SimulatedTimeProvider,
|
|
25
|
+
SimulationDataConfig,
|
|
26
|
+
SimulationSetup,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SimulationRunner:
|
|
31
|
+
"""
|
|
32
|
+
A wrapper around the StrategyContext that encapsulates the simulation logic.
|
|
33
|
+
This class is responsible for running a backtest context from a start time to an end time.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
setup: SimulationSetup
|
|
37
|
+
data_config: SimulationDataConfig
|
|
38
|
+
start: pd.Timestamp
|
|
39
|
+
stop: pd.Timestamp
|
|
40
|
+
account_id: str
|
|
41
|
+
portfolio_log_freq: str
|
|
42
|
+
ctx: IStrategyContext
|
|
43
|
+
data_provider: SimulatedDataProvider
|
|
44
|
+
logs_writer: InMemoryLogsWriter
|
|
45
|
+
|
|
46
|
+
strategy_params: dict[str, Any]
|
|
47
|
+
strategy_class: str
|
|
48
|
+
|
|
49
|
+
# adjusted times
|
|
50
|
+
_stop: pd.Timestamp | None = None
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
setup: SimulationSetup,
|
|
55
|
+
data_config: SimulationDataConfig,
|
|
56
|
+
start: pd.Timestamp | str,
|
|
57
|
+
stop: pd.Timestamp | str,
|
|
58
|
+
account_id: str = "SimulatedAccount",
|
|
59
|
+
portfolio_log_freq: str = "5Min",
|
|
60
|
+
):
|
|
61
|
+
"""
|
|
62
|
+
Initialize the BacktestContextRunner with a strategy context.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
setup (SimulationSetup): The setup to run.
|
|
66
|
+
data_config (SimulationDataConfig): The data setup to use.
|
|
67
|
+
start (pd.Timestamp): The start time of the simulation.
|
|
68
|
+
stop (pd.Timestamp): The end time of the simulation.
|
|
69
|
+
account_id (str): The account id to use.
|
|
70
|
+
portfolio_log_freq (str): The portfolio log frequency to use.
|
|
71
|
+
"""
|
|
72
|
+
self.setup = setup
|
|
73
|
+
self.data_config = data_config
|
|
74
|
+
self.start = pd.Timestamp(start)
|
|
75
|
+
self.stop = pd.Timestamp(stop)
|
|
76
|
+
self.account_id = account_id
|
|
77
|
+
self.portfolio_log_freq = portfolio_log_freq
|
|
78
|
+
self.ctx = self._create_backtest_context()
|
|
79
|
+
|
|
80
|
+
# - get strategy parameters BEFORE simulation start
|
|
81
|
+
# potentially strategy may change it's parameters during simulation
|
|
82
|
+
self.strategy_params = {}
|
|
83
|
+
self.strategy_class = ""
|
|
84
|
+
if self.setup.setup_type in [SetupTypes.STRATEGY, SetupTypes.STRATEGY_AND_TRACKER]:
|
|
85
|
+
self.strategy_params = extract_parameters_from_object(self.setup.generator)
|
|
86
|
+
self.strategy_class = full_qualified_class_name(self.setup.generator)
|
|
87
|
+
|
|
88
|
+
def run(self, silent: bool = False):
|
|
89
|
+
"""
|
|
90
|
+
Run the backtest from start to stop.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
start (pd.Timestamp | str): The start time of the simulation.
|
|
94
|
+
stop (pd.Timestamp | str): The end time of the simulation.
|
|
95
|
+
silent (bool, optional): Whether to suppress progress output. Defaults to False.
|
|
96
|
+
"""
|
|
97
|
+
logger.debug(f"[<y>BacktestContextRunner</y>] :: Running simulation from {self.start} to {self.stop}")
|
|
98
|
+
|
|
99
|
+
# Start the context
|
|
100
|
+
self.ctx.start()
|
|
101
|
+
|
|
102
|
+
# Apply default warmup periods if strategy didn't set them
|
|
103
|
+
for s in self.ctx.get_subscriptions():
|
|
104
|
+
if not self.ctx.get_warmup(s) and (_d_wt := self.data_config.default_warmups.get(s)):
|
|
105
|
+
logger.debug(
|
|
106
|
+
f"[<y>BacktestContextRunner</y>] :: Strategy didn't set warmup period for <c>{s}</c> so default <c>{_d_wt}</c> will be used"
|
|
107
|
+
)
|
|
108
|
+
self.ctx.set_warmup({s: _d_wt})
|
|
109
|
+
|
|
110
|
+
# Subscribe to any custom data types if needed
|
|
111
|
+
def _is_known_type(t: str):
|
|
112
|
+
try:
|
|
113
|
+
DataType(t)
|
|
114
|
+
return True
|
|
115
|
+
except: # noqa: E722
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
for t, r in self.data_config.data_providers.items():
|
|
119
|
+
if not _is_known_type(t) or t in [
|
|
120
|
+
DataType.TRADE,
|
|
121
|
+
DataType.OHLC_TRADES,
|
|
122
|
+
DataType.OHLC_QUOTES,
|
|
123
|
+
DataType.QUOTE,
|
|
124
|
+
DataType.ORDERBOOK,
|
|
125
|
+
]:
|
|
126
|
+
logger.debug(f"[<y>BacktestContextRunner</y>] :: Subscribing to: {t}")
|
|
127
|
+
self.ctx.subscribe(t, self.ctx.instruments)
|
|
128
|
+
|
|
129
|
+
stop = self._stop or self.stop
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
# Run the data provider
|
|
133
|
+
self.data_provider.run(self.start, stop, silent=silent)
|
|
134
|
+
except KeyboardInterrupt:
|
|
135
|
+
logger.error("Simulated trading interrupted by user!")
|
|
136
|
+
finally:
|
|
137
|
+
# Stop the context
|
|
138
|
+
self.ctx.stop()
|
|
139
|
+
|
|
140
|
+
def print_latency_report(self) -> None:
|
|
141
|
+
_l_r = SW.latency_report()
|
|
142
|
+
if _l_r is not None:
|
|
143
|
+
logger.info(
|
|
144
|
+
"<BLUE> Time spent in simulation report </BLUE>\n<r>"
|
|
145
|
+
+ _frame_to_str(
|
|
146
|
+
_l_r.sort_values("latency", ascending=False).reset_index(drop=True), "simulation", -1, -1, False
|
|
147
|
+
)
|
|
148
|
+
+ "</r>"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def _create_backtest_context(self) -> IStrategyContext:
|
|
152
|
+
tcc = lookup.fees.find(self.setup.exchange.lower(), self.setup.commissions)
|
|
153
|
+
if tcc is None:
|
|
154
|
+
raise SimulationConfigError(
|
|
155
|
+
f"Can't find transaction costs calculator for '{self.setup.exchange}' for specification '{self.setup.commissions}' !"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
channel = SimulatedCtrlChannel("databus", sentinel=(None, None, None, None))
|
|
159
|
+
simulated_clock = SimulatedTimeProvider(np.datetime64(self.start, "ns"))
|
|
160
|
+
|
|
161
|
+
logger.debug(
|
|
162
|
+
f"[<y>simulator</y>] :: Preparing simulated trading on <g>{self.setup.exchange.upper()}</g> for {self.setup.capital} {self.setup.base_currency}..."
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
account = SimulatedAccountProcessor(
|
|
166
|
+
account_id=self.account_id,
|
|
167
|
+
channel=channel,
|
|
168
|
+
base_currency=self.setup.base_currency,
|
|
169
|
+
initial_capital=self.setup.capital,
|
|
170
|
+
time_provider=simulated_clock,
|
|
171
|
+
tcc=tcc,
|
|
172
|
+
accurate_stop_orders_execution=self.setup.accurate_stop_orders_execution,
|
|
173
|
+
)
|
|
174
|
+
scheduler = SimulatedScheduler(channel, lambda: simulated_clock.time().item())
|
|
175
|
+
broker = SimulatedBroker(channel, account, self.setup.exchange)
|
|
176
|
+
data_provider = SimulatedDataProvider(
|
|
177
|
+
exchange_id=self.setup.exchange,
|
|
178
|
+
channel=channel,
|
|
179
|
+
scheduler=scheduler,
|
|
180
|
+
time_provider=simulated_clock,
|
|
181
|
+
account=account,
|
|
182
|
+
readers=self.data_config.data_providers,
|
|
183
|
+
open_close_time_indent_secs=self.data_config.adjusted_open_close_time_indent_secs,
|
|
184
|
+
)
|
|
185
|
+
# - get aux data provider
|
|
186
|
+
_aux_data = self.data_config.get_timeguarded_aux_reader(simulated_clock)
|
|
187
|
+
# - it will store simulation results into memory
|
|
188
|
+
logs_writer = InMemoryLogsWriter(self.account_id, self.setup.name, "0")
|
|
189
|
+
|
|
190
|
+
# - it will store simulation results into memory
|
|
191
|
+
strat: IStrategy | None = None
|
|
192
|
+
|
|
193
|
+
match self.setup.setup_type:
|
|
194
|
+
case SetupTypes.STRATEGY:
|
|
195
|
+
strat = self.setup.generator # type: ignore
|
|
196
|
+
|
|
197
|
+
case SetupTypes.STRATEGY_AND_TRACKER:
|
|
198
|
+
strat = self.setup.generator # type: ignore
|
|
199
|
+
strat.tracker = lambda ctx: self.setup.tracker # type: ignore
|
|
200
|
+
|
|
201
|
+
case SetupTypes.SIGNAL:
|
|
202
|
+
strat = SignalsProxy(timeframe=self.setup.signal_timeframe)
|
|
203
|
+
data_provider.set_generated_signals(self.setup.generator) # type: ignore
|
|
204
|
+
|
|
205
|
+
# - we don't need any unexpected triggerings
|
|
206
|
+
self._stop = min(self.setup.generator.index[-1], self.stop) # type: ignore
|
|
207
|
+
|
|
208
|
+
case SetupTypes.SIGNAL_AND_TRACKER:
|
|
209
|
+
strat = SignalsProxy(timeframe=self.setup.signal_timeframe)
|
|
210
|
+
strat.tracker = lambda ctx: self.setup.tracker
|
|
211
|
+
data_provider.set_generated_signals(self.setup.generator) # type: ignore
|
|
212
|
+
|
|
213
|
+
# - we don't need any unexpected triggerings
|
|
214
|
+
self._stop = min(self.setup.generator.index[-1], self.stop) # type: ignore
|
|
215
|
+
|
|
216
|
+
case _:
|
|
217
|
+
raise SimulationError(f"Unsupported setup type: {self.setup.setup_type} !")
|
|
218
|
+
|
|
219
|
+
if not isinstance(strat, IStrategy):
|
|
220
|
+
raise SimulationConfigError(f"Strategy should be an instance of IStrategy, but got {strat} !")
|
|
221
|
+
|
|
222
|
+
ctx = StrategyContext(
|
|
223
|
+
strategy=strat,
|
|
224
|
+
broker=broker,
|
|
225
|
+
data_provider=data_provider,
|
|
226
|
+
account=account,
|
|
227
|
+
scheduler=scheduler,
|
|
228
|
+
time_provider=simulated_clock,
|
|
229
|
+
instruments=self.setup.instruments,
|
|
230
|
+
logging=StrategyLogging(logs_writer, portfolio_log_freq=self.portfolio_log_freq),
|
|
231
|
+
aux_data_provider=_aux_data,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# - setup base subscription from spec
|
|
235
|
+
if ctx.get_base_subscription() == DataType.NONE:
|
|
236
|
+
logger.debug(
|
|
237
|
+
f"[<y>simulator</y>] :: Setting up default base subscription: {self.data_config.default_base_subscription}"
|
|
238
|
+
)
|
|
239
|
+
ctx.set_base_subscription(self.data_config.default_base_subscription)
|
|
240
|
+
|
|
241
|
+
# - set default on_event schedule if detected and strategy didn't set it's own schedule
|
|
242
|
+
if not ctx.get_event_schedule("time") and self.data_config.default_trigger_schedule:
|
|
243
|
+
logger.debug(f"[<y>simulator</y>] :: Setting default schedule: {self.data_config.default_trigger_schedule}")
|
|
244
|
+
ctx.set_event_schedule(self.data_config.default_trigger_schedule)
|
|
245
|
+
|
|
246
|
+
self.data_provider = data_provider
|
|
247
|
+
self.logs_writer = logs_writer
|
|
248
|
+
return ctx
|