Qubx 0.6.35__tar.gz → 0.6.37__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.35 → qubx-0.6.37}/PKG-INFO +1 -1
- {qubx-0.6.35 → qubx-0.6.37}/pyproject.toml +1 -1
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/broker.py +68 -44
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/exchanges/__init__.py +1 -1
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +7 -2
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +23 -8
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/utils.py +2 -2
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/tardis/data.py +1 -1
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/context.py +13 -2
- qubx-0.6.37/src/qubx/core/errors.py +51 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/interfaces.py +11 -2
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/metrics.py +26 -4
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/data/hft.py +37 -9
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/data/readers.py +22 -22
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/features/core.py +8 -7
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/runner/_jupyter_runner.pyt +8 -1
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/runner/runner.py +1 -1
- qubx-0.6.35/src/qubx/core/errors.py +0 -32
- {qubx-0.6.35 → qubx-0.6.37}/LICENSE +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/README.md +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/build.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/__init__.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/_nb_magic.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/backtester/__init__.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/backtester/account.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/backtester/broker.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/backtester/data.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/backtester/management.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/backtester/ome.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/backtester/optimization.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/backtester/runner.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/backtester/simulated_data.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/backtester/simulated_exchange.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/backtester/simulator.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/backtester/utils.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/cli/__init__.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/cli/commands.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/cli/deploy.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/cli/misc.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/cli/release.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/__init__.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/account.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/data.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/exceptions.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/factory.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/reader.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/tardis/utils.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/__init__.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/account.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/basics.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/deque.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/exceptions.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/helpers.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/initializer.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/loggers.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/lookups.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/mixins/__init__.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/mixins/market.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/mixins/processing.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/mixins/subscription.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/mixins/trading.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/mixins/universe.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/series.pxd +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/series.pyi +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/series.pyx +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/utils.pyi +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/utils.pyx +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/data/__init__.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/data/composite.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/data/helpers.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/data/registry.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/data/tardis.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/emitters/__init__.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/emitters/base.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/emitters/composite.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/emitters/csv.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/emitters/prometheus.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/emitters/questdb.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/exporters/__init__.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/exporters/composite.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/exporters/formatters/__init__.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/exporters/formatters/base.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/exporters/formatters/incremental.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/exporters/formatters/slack.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/exporters/redis_streams.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/exporters/slack.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/features/__init__.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/features/orderbook.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/features/price.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/features/trades.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/features/utils.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/gathering/simplest.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/health/__init__.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/health/base.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/math/__init__.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/math/stats.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/notifications/__init__.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/notifications/composite.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/notifications/slack.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/pandaz/__init__.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/pandaz/ta.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/pandaz/utils.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/resources/_build.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/resources/instruments/symbols-binance.cm.json +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/resources/instruments/symbols-binance.json +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/resources/instruments/symbols-binance.um.json +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/resources/instruments/symbols-bitfinex.f.json +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/resources/instruments/symbols-bitfinex.json +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/resources/instruments/symbols-kraken.f.json +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/resources/instruments/symbols-kraken.json +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/restarts/__init__.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/restarts/state_resolvers.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/restarts/time_finders.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/restorers/__init__.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/restorers/balance.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/restorers/factory.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/restorers/interfaces.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/restorers/position.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/restorers/signal.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/restorers/state.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/restorers/utils.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/ta/__init__.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/ta/indicators.pxd +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/ta/indicators.pyi +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/ta/indicators.pyx +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/trackers/__init__.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/trackers/advanced.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/trackers/composite.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/trackers/rebalancers.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/trackers/riskctrl.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/trackers/sizers.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/__init__.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/_pyxreloader.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/charting/lookinglass.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/charting/mpl_helpers.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/collections.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/marketdata/binance.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/marketdata/ccxt.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/marketdata/dukas.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/misc.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/ntp.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/numbers_utils.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/orderbook.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/plotting/__init__.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/plotting/dashboard.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/plotting/data.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/plotting/interfaces.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/questdb.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/runner/__init__.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/runner/accounts.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/runner/configs.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/runner/factory.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/time.py +0 -0
- {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/version.py +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.37"
|
|
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"
|
|
@@ -14,7 +14,7 @@ from qubx.core.basics import (
|
|
|
14
14
|
Order,
|
|
15
15
|
OrderSide,
|
|
16
16
|
)
|
|
17
|
-
from qubx.core.errors import OrderCancellationError, OrderCreationError, create_error_event
|
|
17
|
+
from qubx.core.errors import ErrorLevel, OrderCancellationError, OrderCreationError, create_error_event
|
|
18
18
|
from qubx.core.exceptions import BadRequest, InvalidOrderParameters
|
|
19
19
|
from qubx.core.interfaces import (
|
|
20
20
|
IAccountProcessor,
|
|
@@ -61,6 +61,57 @@ class CcxtBroker(IBroker):
|
|
|
61
61
|
def is_simulated_trading(self) -> bool:
|
|
62
62
|
return False
|
|
63
63
|
|
|
64
|
+
def _post_order_error_to_databus(
|
|
65
|
+
self,
|
|
66
|
+
error: Exception,
|
|
67
|
+
instrument: Instrument,
|
|
68
|
+
order_side: OrderSide,
|
|
69
|
+
order_type: str,
|
|
70
|
+
amount: float,
|
|
71
|
+
price: float | None,
|
|
72
|
+
client_id: str | None,
|
|
73
|
+
time_in_force: str,
|
|
74
|
+
**options,
|
|
75
|
+
):
|
|
76
|
+
level = ErrorLevel.LOW
|
|
77
|
+
match error:
|
|
78
|
+
case ccxt.InsufficientFunds():
|
|
79
|
+
level = ErrorLevel.HIGH
|
|
80
|
+
logger.error(
|
|
81
|
+
f"(::create_order) INSUFFICIENT FUNDS for {order_side} {amount} {order_type} for {instrument.symbol} : {error}"
|
|
82
|
+
)
|
|
83
|
+
case ccxt.OrderNotFillable():
|
|
84
|
+
level = ErrorLevel.LOW
|
|
85
|
+
logger.error(
|
|
86
|
+
f"(::create_order) ORDER NOT FILLEABLE for {order_side} {amount} {order_type} for [{instrument.symbol}] : {error}"
|
|
87
|
+
)
|
|
88
|
+
case ccxt.InvalidOrder():
|
|
89
|
+
level = ErrorLevel.LOW
|
|
90
|
+
logger.error(
|
|
91
|
+
f"(::create_order) INVALID ORDER for {order_side} {amount} {order_type} for {instrument.symbol} : {error}"
|
|
92
|
+
)
|
|
93
|
+
case ccxt.BadRequest():
|
|
94
|
+
level = ErrorLevel.LOW
|
|
95
|
+
logger.error(
|
|
96
|
+
f"(::create_order) BAD REQUEST for {order_side} {amount} {order_type} for {instrument.symbol} : {error}"
|
|
97
|
+
)
|
|
98
|
+
case _:
|
|
99
|
+
level = ErrorLevel.MEDIUM
|
|
100
|
+
logger.error(f"(::create_order) Unexpected error: {error}")
|
|
101
|
+
|
|
102
|
+
error_event = OrderCreationError(
|
|
103
|
+
timestamp=self.time_provider.time(),
|
|
104
|
+
message=f"Error message: {str(error)}",
|
|
105
|
+
level=level,
|
|
106
|
+
instrument=instrument,
|
|
107
|
+
amount=amount,
|
|
108
|
+
price=price,
|
|
109
|
+
order_type=order_type,
|
|
110
|
+
side=order_side,
|
|
111
|
+
error=error,
|
|
112
|
+
)
|
|
113
|
+
self.channel.send(create_error_event(error_event))
|
|
114
|
+
|
|
64
115
|
def send_order_async(
|
|
65
116
|
self,
|
|
66
117
|
instrument: Instrument,
|
|
@@ -93,33 +144,20 @@ class CcxtBroker(IBroker):
|
|
|
93
144
|
)
|
|
94
145
|
|
|
95
146
|
if error:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
timestamp=self.time_provider.time(),
|
|
99
|
-
message=str(error),
|
|
100
|
-
instrument=instrument,
|
|
101
|
-
amount=amount,
|
|
102
|
-
price=price,
|
|
103
|
-
order_type=order_type,
|
|
104
|
-
side=order_side,
|
|
147
|
+
self._post_order_error_to_databus(
|
|
148
|
+
error, instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
|
|
105
149
|
)
|
|
106
|
-
|
|
107
|
-
|
|
150
|
+
order = None
|
|
151
|
+
|
|
108
152
|
return order
|
|
153
|
+
|
|
109
154
|
except Exception as err:
|
|
110
155
|
# Catch any unexpected errors and send them through the channel as well
|
|
111
|
-
logger.error(f"Unexpected error in async order creation: {err}")
|
|
156
|
+
logger.error(f"{self.__class__.__name__} :: Unexpected error in async order creation: {err}")
|
|
112
157
|
logger.error(traceback.format_exc())
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
message=f"Unexpected error: {str(err)}",
|
|
116
|
-
instrument=instrument,
|
|
117
|
-
amount=amount,
|
|
118
|
-
price=price,
|
|
119
|
-
order_type=order_type,
|
|
120
|
-
side=order_side,
|
|
158
|
+
self._post_order_error_to_databus(
|
|
159
|
+
err, instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
|
|
121
160
|
)
|
|
122
|
-
self.channel.send(create_error_event(error_event))
|
|
123
161
|
return None
|
|
124
162
|
|
|
125
163
|
# Submit the task to the async loop
|
|
@@ -135,7 +173,7 @@ class CcxtBroker(IBroker):
|
|
|
135
173
|
client_id: str | None = None,
|
|
136
174
|
time_in_force: str = "gtc",
|
|
137
175
|
**options,
|
|
138
|
-
) -> Order:
|
|
176
|
+
) -> Order | None:
|
|
139
177
|
"""
|
|
140
178
|
Submit an order and wait for the result. Exceptions will be raised on errors.
|
|
141
179
|
|
|
@@ -169,14 +207,16 @@ class CcxtBroker(IBroker):
|
|
|
169
207
|
|
|
170
208
|
# If there was no error but also no order, something went wrong
|
|
171
209
|
if not order and not self.enable_create_order_ws:
|
|
172
|
-
raise ExchangeError("Order creation failed with no specific error")
|
|
210
|
+
raise ExchangeError(f"{self.__class__.__name__} :: Order creation failed with no specific error")
|
|
173
211
|
|
|
174
212
|
return order
|
|
175
213
|
|
|
176
214
|
except Exception as err:
|
|
177
215
|
# This will catch any errors from future.result() or if we explicitly raise an error
|
|
178
|
-
|
|
179
|
-
|
|
216
|
+
self._post_order_error_to_databus(
|
|
217
|
+
err, instrument, order_side, order_type, amount, price, client_id, time_in_force, **options
|
|
218
|
+
)
|
|
219
|
+
return None
|
|
180
220
|
|
|
181
221
|
def cancel_order(self, order_id: str) -> Order | None:
|
|
182
222
|
orders = self.account.get_orders()
|
|
@@ -231,25 +271,7 @@ class CcxtBroker(IBroker):
|
|
|
231
271
|
logger.info(f"New order {order}")
|
|
232
272
|
return order, None
|
|
233
273
|
|
|
234
|
-
except ccxt.OrderNotFillable as exc:
|
|
235
|
-
logger.error(
|
|
236
|
-
f"(::_create_order) [{instrument.symbol}] ORDER NOT FILLEABLE for {order_side} {amount} {order_type} : {exc}"
|
|
237
|
-
)
|
|
238
|
-
return None, exc
|
|
239
|
-
except ccxt.InvalidOrder as exc:
|
|
240
|
-
logger.error(
|
|
241
|
-
f"(::_create_order) INVALID ORDER for {order_side} {amount} {order_type} for {instrument.symbol} : {exc}"
|
|
242
|
-
)
|
|
243
|
-
return None, exc
|
|
244
|
-
except ccxt.BadRequest as exc:
|
|
245
|
-
logger.error(
|
|
246
|
-
f"(::_create_order) BAD REQUEST for {order_side} {amount} {order_type} for {instrument.symbol} : {exc}"
|
|
247
|
-
)
|
|
248
|
-
return None, exc
|
|
249
274
|
except Exception as err:
|
|
250
|
-
logger.error(
|
|
251
|
-
f"(::_create_order) {order_side} {amount} {order_type} for {instrument.symbol} exception : {err}"
|
|
252
|
-
)
|
|
253
275
|
return None, err
|
|
254
276
|
|
|
255
277
|
def _prepare_order_payload(
|
|
@@ -371,6 +393,8 @@ class CcxtBroker(IBroker):
|
|
|
371
393
|
order_id=order_id,
|
|
372
394
|
message=f"Timeout reached for canceling order {order_id}",
|
|
373
395
|
instrument=instrument,
|
|
396
|
+
level=ErrorLevel.LOW,
|
|
397
|
+
error=None,
|
|
374
398
|
)
|
|
375
399
|
)
|
|
376
400
|
)
|
|
@@ -25,7 +25,7 @@ EXCHANGE_ALIASES = {
|
|
|
25
25
|
|
|
26
26
|
CUSTOM_BROKERS = {
|
|
27
27
|
"binance": partial(BinanceCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=False),
|
|
28
|
-
"binance.um": partial(BinanceCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=
|
|
28
|
+
"binance.um": partial(BinanceCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=True),
|
|
29
29
|
"binance.cm": partial(BinanceCcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=False),
|
|
30
30
|
"binance.pm": partial(BinanceCcxtBroker, enable_create_order_ws=False, enable_cancel_order_ws=False),
|
|
31
31
|
"bitfinex.f": partial(CcxtBroker, enable_create_order_ws=True, enable_cancel_order_ws=True),
|
|
@@ -3,7 +3,7 @@ from typing import Dict, List
|
|
|
3
3
|
import ccxt.pro as cxp
|
|
4
4
|
from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheByTimestamp
|
|
5
5
|
from ccxt.async_support.base.ws.client import Client
|
|
6
|
-
from ccxt.base.errors import ArgumentsRequired, BadRequest, NotSupported
|
|
6
|
+
from ccxt.base.errors import ArgumentsRequired, BadRequest, InsufficientFunds, NotSupported
|
|
7
7
|
from ccxt.base.precise import Precise
|
|
8
8
|
from ccxt.base.types import (
|
|
9
9
|
Any,
|
|
@@ -34,7 +34,12 @@ class BinanceQV(cxp.binance):
|
|
|
34
34
|
"name": "aggTrade",
|
|
35
35
|
},
|
|
36
36
|
"localOrderBookLimit": 10_000, # set a large limit to avoid cutting off the orderbook
|
|
37
|
-
}
|
|
37
|
+
},
|
|
38
|
+
"exceptions": {
|
|
39
|
+
"exact": {
|
|
40
|
+
"-2019": InsufficientFunds, # ccxt doesn't have this code for some weird reason !!
|
|
41
|
+
},
|
|
42
|
+
},
|
|
38
43
|
},
|
|
39
44
|
)
|
|
40
45
|
|
|
@@ -78,6 +78,10 @@ class BitfinexF(cxp.bitfinex):
|
|
|
78
78
|
# GTX is not supported by bitfinex, so we need to convert it to PO
|
|
79
79
|
params["timeInForce"] = "PO"
|
|
80
80
|
params["postOnly"] = True
|
|
81
|
+
|
|
82
|
+
if "lev" not in params:
|
|
83
|
+
params["lev"] = 2
|
|
84
|
+
|
|
81
85
|
response = await super().create_order(symbol, type, side, amount, price, params)
|
|
82
86
|
return response
|
|
83
87
|
|
|
@@ -90,6 +94,9 @@ class BitfinexF(cxp.bitfinex):
|
|
|
90
94
|
params["timeInForce"] = "PO"
|
|
91
95
|
params["postOnly"] = True
|
|
92
96
|
|
|
97
|
+
if "lev" not in params:
|
|
98
|
+
params["lev"] = 2
|
|
99
|
+
|
|
93
100
|
await self.load_markets()
|
|
94
101
|
market = self.market(symbol)
|
|
95
102
|
request = self.create_order_request(symbol, type, side, amount, price, params)
|
|
@@ -98,14 +105,22 @@ class BitfinexF(cxp.bitfinex):
|
|
|
98
105
|
# request["cid"] = request["newClientOrderId"]
|
|
99
106
|
# del request["newClientOrderId"]
|
|
100
107
|
|
|
101
|
-
|
|
102
|
-
type
|
|
103
|
-
symbol
|
|
104
|
-
amount
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
_params = {
|
|
109
|
+
"type": request["type"],
|
|
110
|
+
"symbol": request["symbol"],
|
|
111
|
+
"amount": float(request["amount"]),
|
|
112
|
+
"lev": request["lev"],
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if "price" in request:
|
|
116
|
+
_params["price"] = float(request["price"])
|
|
117
|
+
else:
|
|
118
|
+
_params["price"] = None
|
|
119
|
+
|
|
120
|
+
if "flags" in request:
|
|
121
|
+
_params["flags"] = request["flags"]
|
|
122
|
+
|
|
123
|
+
await self.bfx.wss.inputs.submit_order(**_params)
|
|
109
124
|
return self.safe_order({"info": {}}, market) # type: ignore
|
|
110
125
|
|
|
111
126
|
async def cancel_order_ws(self, id: str, symbol: Str = None, params={}) -> Order | None:
|
|
@@ -54,7 +54,7 @@ def ccxt_convert_order_info(instrument: Instrument, raw: dict[str, Any]) -> Orde
|
|
|
54
54
|
type=_type,
|
|
55
55
|
instrument=instrument,
|
|
56
56
|
time=pd.Timestamp(raw["timestamp"], unit="ms"), # type: ignore
|
|
57
|
-
quantity=amnt,
|
|
57
|
+
quantity=abs(amnt) * (-1 if side == "SELL" else 1),
|
|
58
58
|
price=float(price) if price is not None else 0.0,
|
|
59
59
|
side=side,
|
|
60
60
|
status=status.upper(),
|
|
@@ -157,7 +157,7 @@ def ccxt_convert_positions(
|
|
|
157
157
|
)
|
|
158
158
|
pos = Position(
|
|
159
159
|
instrument=instr,
|
|
160
|
-
quantity=info["contracts"] * (-1 if info["side"] == "short" else 1),
|
|
160
|
+
quantity=abs(info["contracts"]) * (-1 if info["side"] == "short" else 1),
|
|
161
161
|
pos_average_price=info["entryPrice"],
|
|
162
162
|
)
|
|
163
163
|
if info.get("markPrice", None) is not None:
|
|
@@ -454,7 +454,7 @@ class TardisDataProvider(IDataProvider):
|
|
|
454
454
|
|
|
455
455
|
# Record data arrival for health monitoring
|
|
456
456
|
tardis_type = data["type"]
|
|
457
|
-
tardis_name = data["name"]
|
|
457
|
+
tardis_name = data["name"] if "name" in data else ""
|
|
458
458
|
qubx_type = self._map_tardis_type_to_data_type(tardis_type)
|
|
459
459
|
if qubx_type:
|
|
460
460
|
self._health_monitor.record_data_arrival(qubx_type, dt_64(msg_time, "ns"))
|
|
@@ -16,6 +16,7 @@ from qubx.core.basics import (
|
|
|
16
16
|
Timestamped,
|
|
17
17
|
dt_64,
|
|
18
18
|
)
|
|
19
|
+
from qubx.core.errors import BaseErrorEvent, ErrorLevel
|
|
19
20
|
from qubx.core.exceptions import StrategyExceededMaxNumberOfRuntimeFailuresError
|
|
20
21
|
from qubx.core.helpers import (
|
|
21
22
|
BasicScheduler,
|
|
@@ -286,7 +287,7 @@ class StrategyContext(IStrategyContext):
|
|
|
286
287
|
|
|
287
288
|
# - invoke strategy's stop code
|
|
288
289
|
try:
|
|
289
|
-
if not self.
|
|
290
|
+
if not self.is_warmup_in_progress:
|
|
290
291
|
self.strategy.on_stop(self)
|
|
291
292
|
except Exception as strat_error:
|
|
292
293
|
logger.error(
|
|
@@ -327,7 +328,7 @@ class StrategyContext(IStrategyContext):
|
|
|
327
328
|
return self._data_providers[0].is_simulation
|
|
328
329
|
|
|
329
330
|
@property
|
|
330
|
-
def
|
|
331
|
+
def is_paper_trading(self) -> bool:
|
|
331
332
|
return self._brokers[0].is_simulated_trading
|
|
332
333
|
|
|
333
334
|
# IAccountViewer delegation
|
|
@@ -536,6 +537,16 @@ class StrategyContext(IStrategyContext):
|
|
|
536
537
|
if _should_record:
|
|
537
538
|
self._health_monitor.record_start_processing(d_type, dt_64(data.time, "ns"))
|
|
538
539
|
|
|
540
|
+
# - notify error if error level is medium or higher
|
|
541
|
+
if (
|
|
542
|
+
self._lifecycle_notifier
|
|
543
|
+
and isinstance(data, BaseErrorEvent)
|
|
544
|
+
and data.level.value >= ErrorLevel.MEDIUM.value
|
|
545
|
+
):
|
|
546
|
+
self._lifecycle_notifier.notify_error(
|
|
547
|
+
self._strategy_name, data.error or Exception("Unknown error"), {"message": str(data)}
|
|
548
|
+
)
|
|
549
|
+
|
|
539
550
|
if self.process_data(instrument, d_type, data, hist):
|
|
540
551
|
channel.stop()
|
|
541
552
|
break
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Error types that are sent through the event channel.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
from qubx.core.basics import Instrument, dt_64
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ErrorLevel(Enum):
|
|
12
|
+
LOW = 1 # continue trading
|
|
13
|
+
MEDIUM = 2 # send notifications and continue trading
|
|
14
|
+
HIGH = 3 # send notification and cancel orders and close positions
|
|
15
|
+
CRITICAL = 4 # send notification and shutdown strategy
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class BaseErrorEvent:
|
|
20
|
+
timestamp: dt_64
|
|
21
|
+
message: str
|
|
22
|
+
level: ErrorLevel
|
|
23
|
+
error: Exception | None
|
|
24
|
+
|
|
25
|
+
def __str__(self):
|
|
26
|
+
return f"[{self.level}] : {self.timestamp} : {self.message} / {self.error}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def create_error_event(error: BaseErrorEvent) -> tuple[None, str, BaseErrorEvent, bool]:
|
|
30
|
+
return None, "error", error, False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class OrderCreationError(BaseErrorEvent):
|
|
35
|
+
instrument: Instrument
|
|
36
|
+
amount: float
|
|
37
|
+
price: float | None
|
|
38
|
+
order_type: str
|
|
39
|
+
side: str
|
|
40
|
+
|
|
41
|
+
def __str__(self):
|
|
42
|
+
return f"[{self.level}] : {self.timestamp} : {self.message} / {self.error} ||| Order creation error for {self.order_type} {self.side} {self.instrument} {self.amount}"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class OrderCancellationError(BaseErrorEvent):
|
|
47
|
+
instrument: Instrument
|
|
48
|
+
order_id: str
|
|
49
|
+
|
|
50
|
+
def __str__(self):
|
|
51
|
+
return f"[{self.level}] : {self.timestamp} : {self.message} / {self.error} ||| Order cancellation error for {self.order_id} {self.instrument}"
|
|
@@ -1068,7 +1068,6 @@ class IStrategyContext(
|
|
|
1068
1068
|
IProcessingManager,
|
|
1069
1069
|
IAccountViewer,
|
|
1070
1070
|
IWarmupStateSaver,
|
|
1071
|
-
StrategyState,
|
|
1072
1071
|
):
|
|
1073
1072
|
strategy: "IStrategy"
|
|
1074
1073
|
initializer: "IStrategyInitializer"
|
|
@@ -1086,17 +1085,27 @@ class IStrategyContext(
|
|
|
1086
1085
|
"""Stop the strategy context."""
|
|
1087
1086
|
pass
|
|
1088
1087
|
|
|
1088
|
+
@property
|
|
1089
|
+
def state(self) -> StrategyState:
|
|
1090
|
+
"""Get the strategy state."""
|
|
1091
|
+
return StrategyState(**self._strategy_state.__dict__)
|
|
1092
|
+
|
|
1089
1093
|
def is_running(self) -> bool:
|
|
1090
1094
|
"""Check if the strategy context is running."""
|
|
1091
1095
|
return False
|
|
1092
1096
|
|
|
1097
|
+
@property
|
|
1098
|
+
def is_warmup_in_progress(self) -> bool:
|
|
1099
|
+
"""Check if the warmup is in progress."""
|
|
1100
|
+
return self._strategy_state.is_warmup_in_progress
|
|
1101
|
+
|
|
1093
1102
|
@property
|
|
1094
1103
|
def is_simulation(self) -> bool:
|
|
1095
1104
|
"""Check if the strategy context is running in simulation mode."""
|
|
1096
1105
|
return False
|
|
1097
1106
|
|
|
1098
1107
|
@property
|
|
1099
|
-
def
|
|
1108
|
+
def is_paper_trading(self) -> bool:
|
|
1100
1109
|
"""Check if the strategy context is running in simulated trading mode."""
|
|
1101
1110
|
return False
|
|
1102
1111
|
|
|
@@ -717,7 +717,7 @@ class TradingSessionResult:
|
|
|
717
717
|
"name": self.name,
|
|
718
718
|
"start": pd.Timestamp(self.start).isoformat(),
|
|
719
719
|
"stop": pd.Timestamp(self.stop).isoformat(),
|
|
720
|
-
"
|
|
720
|
+
"exchanges": self.exchanges,
|
|
721
721
|
"capital": self.capital,
|
|
722
722
|
"base_currency": self.base_currency,
|
|
723
723
|
"commissions": self.commissions,
|
|
@@ -824,6 +824,12 @@ class TradingSessionResult:
|
|
|
824
824
|
info = self.info()
|
|
825
825
|
if description:
|
|
826
826
|
info["description"] = description
|
|
827
|
+
# - set name if not specified
|
|
828
|
+
if info.get("name") is None:
|
|
829
|
+
info["name"] = name
|
|
830
|
+
|
|
831
|
+
# - add numpy array representer
|
|
832
|
+
yaml.SafeDumper.add_representer(np.ndarray, lambda dumper, data: dumper.represent_list(data.tolist()))
|
|
827
833
|
yaml.safe_dump(info, f, sort_keys=False, indent=4)
|
|
828
834
|
|
|
829
835
|
# - save logs
|
|
@@ -855,15 +861,31 @@ class TradingSessionResult:
|
|
|
855
861
|
|
|
856
862
|
with zipfile.ZipFile(path, "r") as zip_ref:
|
|
857
863
|
info = yaml.safe_load(zip_ref.read("info.yml"))
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
864
|
+
try:
|
|
865
|
+
portfolio = pd.read_csv(
|
|
866
|
+
zip_ref.open("portfolio.csv"), index_col=["timestamp"], parse_dates=["timestamp"]
|
|
867
|
+
)
|
|
868
|
+
except:
|
|
869
|
+
portfolio = pd.DataFrame()
|
|
870
|
+
try:
|
|
871
|
+
executions = pd.read_csv(
|
|
872
|
+
zip_ref.open("executions.csv"), index_col=["timestamp"], parse_dates=["timestamp"]
|
|
873
|
+
)
|
|
874
|
+
except:
|
|
875
|
+
executions = pd.DataFrame()
|
|
876
|
+
try:
|
|
877
|
+
signals = pd.read_csv(zip_ref.open("signals.csv"), index_col=["timestamp"], parse_dates=["timestamp"])
|
|
878
|
+
except:
|
|
879
|
+
signals = pd.DataFrame()
|
|
861
880
|
|
|
862
881
|
# load result
|
|
863
882
|
_qbx_version = info.pop("qubx_version")
|
|
864
883
|
_decr = info.pop("description", None)
|
|
865
884
|
_perf = info.pop("performance", None)
|
|
866
885
|
info["instruments"] = info.pop("symbols")
|
|
886
|
+
# - fix for old versions
|
|
887
|
+
_exch = info.pop("exchange")
|
|
888
|
+
info["exchanges"] = _exch if isinstance(_exch, list) else [_exch]
|
|
867
889
|
tsr = TradingSessionResult(**info, portfolio_log=portfolio, executions_log=executions, signals_log=signals)
|
|
868
890
|
tsr.qubx_version = _qbx_version
|
|
869
891
|
tsr._metrics = _perf
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import queue
|
|
2
|
-
from collections import defaultdict
|
|
2
|
+
from collections import defaultdict
|
|
3
3
|
from multiprocessing import Event, Process, Queue
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from threading import Thread
|
|
6
|
-
from typing import Any, Iterable, Optional, TypeAlias, TypeVar
|
|
6
|
+
from typing import Any, Iterable, Optional, TypeAlias, TypeVar
|
|
7
7
|
|
|
8
8
|
import numpy as np
|
|
9
9
|
import pandas as pd
|
|
@@ -148,7 +148,7 @@ class HftChunkPrefetcher:
|
|
|
148
148
|
enable_trade="trade" in queues,
|
|
149
149
|
enable_orderbook="orderbook" in queues,
|
|
150
150
|
)
|
|
151
|
-
ctx = reader._get_or_create_context(data_id, start, stop)
|
|
151
|
+
ctx = reader._get_or_create_context(data_id, start.floor("d"), stop)
|
|
152
152
|
instrument = reader._data_id_to_instrument[data_id]
|
|
153
153
|
|
|
154
154
|
# Initialize buffers only for enabled data types
|
|
@@ -205,9 +205,11 @@ class HftChunkPrefetcher:
|
|
|
205
205
|
]
|
|
206
206
|
)
|
|
207
207
|
reader._create_buffer_if_needed("quotes", instrument, (quote_chunksize,), quote_dtype)
|
|
208
|
+
start_time_ns = start.value
|
|
209
|
+
stop_time_ns = stop.value
|
|
208
210
|
|
|
209
211
|
while not stop_event.is_set():
|
|
210
|
-
reader._next_batch(
|
|
212
|
+
stop_reached = reader._next_batch(
|
|
211
213
|
ctx=ctx,
|
|
212
214
|
instrument=instrument,
|
|
213
215
|
chunksize=chunk_args["chunksize"],
|
|
@@ -216,6 +218,8 @@ class HftChunkPrefetcher:
|
|
|
216
218
|
orderbook_period=orderbook_period,
|
|
217
219
|
tick_size_pct=chunk_args["tick_size_pct"],
|
|
218
220
|
depth=chunk_args["depth"],
|
|
221
|
+
start_time=start_time_ns,
|
|
222
|
+
stop_time=stop_time_ns,
|
|
219
223
|
)
|
|
220
224
|
|
|
221
225
|
# Get records for enabled data types
|
|
@@ -270,6 +274,9 @@ class HftChunkPrefetcher:
|
|
|
270
274
|
for data_type in queues:
|
|
271
275
|
reader._mark_processed(data_type, instrument)
|
|
272
276
|
|
|
277
|
+
if stop_reached:
|
|
278
|
+
break
|
|
279
|
+
|
|
273
280
|
except Exception as e:
|
|
274
281
|
error_queue.put(e)
|
|
275
282
|
for queue in queues.values():
|
|
@@ -450,7 +457,9 @@ class HftDataReader(DataReader):
|
|
|
450
457
|
):
|
|
451
458
|
raise ValueError(f"Data type {data_type} is not enabled")
|
|
452
459
|
|
|
453
|
-
|
|
460
|
+
# - handle start and stop
|
|
461
|
+
_start_raw, _stop = handle_start_stop(start, stop, lambda x: pd.Timestamp(x))
|
|
462
|
+
_start = _start_raw.floor("d") # we must to start from day's start
|
|
454
463
|
assert isinstance(_start, pd.Timestamp) and isinstance(_stop, pd.Timestamp)
|
|
455
464
|
|
|
456
465
|
# Check if we need to recreate the prefetcher
|
|
@@ -489,7 +498,7 @@ class HftDataReader(DataReader):
|
|
|
489
498
|
"orderbook_interval": self.orderbook_interval,
|
|
490
499
|
"trade_capacity": self.trade_capacity,
|
|
491
500
|
}
|
|
492
|
-
prefetcher.start(self.path, data_id,
|
|
501
|
+
prefetcher.start(self.path, data_id, _start_raw, _stop, chunk_args)
|
|
493
502
|
logger.debug(f"Started prefetcher for {data_id}")
|
|
494
503
|
self._prefetchers[data_id] = prefetcher
|
|
495
504
|
self._prefetcher_ranges[data_id] = (_start, _stop)
|
|
@@ -620,7 +629,9 @@ class HftDataReader(DataReader):
|
|
|
620
629
|
orderbook_period: int,
|
|
621
630
|
tick_size_pct: float,
|
|
622
631
|
depth: int,
|
|
623
|
-
|
|
632
|
+
start_time: int,
|
|
633
|
+
stop_time: int,
|
|
634
|
+
) -> bool:
|
|
624
635
|
match data_type:
|
|
625
636
|
case "quote":
|
|
626
637
|
if self._instrument_to_quote_index[instrument] > 0 or not self.enable_quote:
|
|
@@ -636,8 +647,11 @@ class HftDataReader(DataReader):
|
|
|
636
647
|
quote_index,
|
|
637
648
|
trade_index,
|
|
638
649
|
orderbook_index,
|
|
650
|
+
stop_reached,
|
|
639
651
|
) = _simulate_hft(
|
|
640
652
|
ctx=ctx,
|
|
653
|
+
start_time=start_time,
|
|
654
|
+
stop_time=stop_time,
|
|
641
655
|
ob_timestamp=self._instrument_to_name_to_buffer["ob_timestamp"][instrument],
|
|
642
656
|
bid_price_buffer=self._instrument_to_name_to_buffer["bid_price"][instrument],
|
|
643
657
|
ask_price_buffer=self._instrument_to_name_to_buffer["ask_price"][instrument],
|
|
@@ -660,6 +674,8 @@ class HftDataReader(DataReader):
|
|
|
660
674
|
self._instrument_to_trade_index[instrument] = trade_index if self.enable_trade else 0
|
|
661
675
|
self._instrument_to_orderbook_index[instrument] = orderbook_index if self.enable_orderbook else 0
|
|
662
676
|
|
|
677
|
+
return stop_reached
|
|
678
|
+
|
|
663
679
|
def _create_backtest_assets(self, files: list[str], instrument: Instrument) -> list[BacktestAsset]:
|
|
664
680
|
mid_price = _get_initial_mid_price(files)
|
|
665
681
|
roi_lb, roi_ub = mid_price / 4, mid_price * 4
|
|
@@ -760,6 +776,8 @@ def _simulate_hft(
|
|
|
760
776
|
trade_buffer: np.ndarray,
|
|
761
777
|
quote_buffer: np.ndarray,
|
|
762
778
|
batch_size: int,
|
|
779
|
+
start_time: int,
|
|
780
|
+
stop_time: int,
|
|
763
781
|
interval: int = 1_000_000_000,
|
|
764
782
|
orderbook_period: int = 1,
|
|
765
783
|
tick_size_pct: float = 0.0,
|
|
@@ -767,12 +785,17 @@ def _simulate_hft(
|
|
|
767
785
|
enable_quote: bool = True,
|
|
768
786
|
enable_trade: bool = True,
|
|
769
787
|
enable_orderbook: bool = True,
|
|
770
|
-
) -> tuple[int, int, int]:
|
|
788
|
+
) -> tuple[int, int, int, bool]:
|
|
771
789
|
orderbook_index = 0
|
|
772
790
|
quote_index = 0
|
|
773
791
|
trade_index = 0
|
|
792
|
+
stop_reached = False
|
|
774
793
|
|
|
775
794
|
while ctx.elapse(interval) == 0 and orderbook_index < batch_size:
|
|
795
|
+
# - skip if we are before the start time
|
|
796
|
+
if ctx.current_timestamp < start_time:
|
|
797
|
+
continue
|
|
798
|
+
|
|
776
799
|
depth = ctx.depth(0)
|
|
777
800
|
|
|
778
801
|
# record quote
|
|
@@ -824,4 +847,9 @@ def _simulate_hft(
|
|
|
824
847
|
ctx.clear_last_trades(0)
|
|
825
848
|
quote_index += 1
|
|
826
849
|
|
|
827
|
-
|
|
850
|
+
# - stop if we reached the stop time
|
|
851
|
+
if ctx.current_timestamp >= stop_time:
|
|
852
|
+
stop_reached = True
|
|
853
|
+
break
|
|
854
|
+
|
|
855
|
+
return quote_index, trade_index, orderbook_index, stop_reached
|