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.

Files changed (159) hide show
  1. {qubx-0.6.35 → qubx-0.6.37}/PKG-INFO +1 -1
  2. {qubx-0.6.35 → qubx-0.6.37}/pyproject.toml +1 -1
  3. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/broker.py +68 -44
  4. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/exchanges/__init__.py +1 -1
  5. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +7 -2
  6. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +23 -8
  7. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/utils.py +2 -2
  8. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/tardis/data.py +1 -1
  9. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/context.py +13 -2
  10. qubx-0.6.37/src/qubx/core/errors.py +51 -0
  11. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/interfaces.py +11 -2
  12. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/metrics.py +26 -4
  13. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/data/hft.py +37 -9
  14. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/data/readers.py +22 -22
  15. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/features/core.py +8 -7
  16. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/runner/_jupyter_runner.pyt +8 -1
  17. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/runner/runner.py +1 -1
  18. qubx-0.6.35/src/qubx/core/errors.py +0 -32
  19. {qubx-0.6.35 → qubx-0.6.37}/LICENSE +0 -0
  20. {qubx-0.6.35 → qubx-0.6.37}/README.md +0 -0
  21. {qubx-0.6.35 → qubx-0.6.37}/build.py +0 -0
  22. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/__init__.py +0 -0
  23. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/_nb_magic.py +0 -0
  24. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/backtester/__init__.py +0 -0
  25. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/backtester/account.py +0 -0
  26. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/backtester/broker.py +0 -0
  27. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/backtester/data.py +0 -0
  28. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/backtester/management.py +0 -0
  29. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/backtester/ome.py +0 -0
  30. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/backtester/optimization.py +0 -0
  31. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/backtester/runner.py +0 -0
  32. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/backtester/simulated_data.py +0 -0
  33. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/backtester/simulated_exchange.py +0 -0
  34. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/backtester/simulator.py +0 -0
  35. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/backtester/utils.py +0 -0
  36. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/cli/__init__.py +0 -0
  37. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/cli/commands.py +0 -0
  38. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/cli/deploy.py +0 -0
  39. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/cli/misc.py +0 -0
  40. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/cli/release.py +0 -0
  41. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/__init__.py +0 -0
  42. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/account.py +0 -0
  43. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/data.py +0 -0
  44. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  45. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  46. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  47. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  48. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/factory.py +0 -0
  49. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/ccxt/reader.py +0 -0
  50. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/connectors/tardis/utils.py +0 -0
  51. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/__init__.py +0 -0
  52. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/account.py +0 -0
  53. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/basics.py +0 -0
  54. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/deque.py +0 -0
  55. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/exceptions.py +0 -0
  56. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/helpers.py +0 -0
  57. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/initializer.py +0 -0
  58. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/loggers.py +0 -0
  59. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/lookups.py +0 -0
  60. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/mixins/__init__.py +0 -0
  61. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/mixins/market.py +0 -0
  62. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/mixins/processing.py +0 -0
  63. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/mixins/subscription.py +0 -0
  64. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/mixins/trading.py +0 -0
  65. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/mixins/universe.py +0 -0
  66. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/series.pxd +0 -0
  67. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/series.pyi +0 -0
  68. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/series.pyx +0 -0
  69. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/utils.pyi +0 -0
  70. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/core/utils.pyx +0 -0
  71. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/data/__init__.py +0 -0
  72. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/data/composite.py +0 -0
  73. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/data/helpers.py +0 -0
  74. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/data/registry.py +0 -0
  75. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/data/tardis.py +0 -0
  76. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/emitters/__init__.py +0 -0
  77. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/emitters/base.py +0 -0
  78. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/emitters/composite.py +0 -0
  79. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/emitters/csv.py +0 -0
  80. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/emitters/prometheus.py +0 -0
  81. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/emitters/questdb.py +0 -0
  82. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/exporters/__init__.py +0 -0
  83. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/exporters/composite.py +0 -0
  84. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/exporters/formatters/__init__.py +0 -0
  85. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/exporters/formatters/base.py +0 -0
  86. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/exporters/formatters/incremental.py +0 -0
  87. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/exporters/formatters/slack.py +0 -0
  88. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/exporters/redis_streams.py +0 -0
  89. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/exporters/slack.py +0 -0
  90. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/features/__init__.py +0 -0
  91. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/features/orderbook.py +0 -0
  92. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/features/price.py +0 -0
  93. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/features/trades.py +0 -0
  94. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/features/utils.py +0 -0
  95. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/gathering/simplest.py +0 -0
  96. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/health/__init__.py +0 -0
  97. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/health/base.py +0 -0
  98. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/math/__init__.py +0 -0
  99. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/math/stats.py +0 -0
  100. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/notifications/__init__.py +0 -0
  101. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/notifications/composite.py +0 -0
  102. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/notifications/slack.py +0 -0
  103. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/pandaz/__init__.py +0 -0
  104. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/pandaz/ta.py +0 -0
  105. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/pandaz/utils.py +0 -0
  106. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/resources/_build.py +0 -0
  107. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/resources/instruments/symbols-binance.cm.json +0 -0
  108. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/resources/instruments/symbols-binance.json +0 -0
  109. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/resources/instruments/symbols-binance.um.json +0 -0
  110. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/resources/instruments/symbols-bitfinex.f.json +0 -0
  111. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/resources/instruments/symbols-bitfinex.json +0 -0
  112. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/resources/instruments/symbols-kraken.f.json +0 -0
  113. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/resources/instruments/symbols-kraken.json +0 -0
  114. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/restarts/__init__.py +0 -0
  115. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/restarts/state_resolvers.py +0 -0
  116. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/restarts/time_finders.py +0 -0
  117. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/restorers/__init__.py +0 -0
  118. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/restorers/balance.py +0 -0
  119. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/restorers/factory.py +0 -0
  120. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/restorers/interfaces.py +0 -0
  121. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/restorers/position.py +0 -0
  122. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/restorers/signal.py +0 -0
  123. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/restorers/state.py +0 -0
  124. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/restorers/utils.py +0 -0
  125. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/ta/__init__.py +0 -0
  126. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/ta/indicators.pxd +0 -0
  127. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/ta/indicators.pyi +0 -0
  128. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/ta/indicators.pyx +0 -0
  129. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/trackers/__init__.py +0 -0
  130. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/trackers/advanced.py +0 -0
  131. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/trackers/composite.py +0 -0
  132. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/trackers/rebalancers.py +0 -0
  133. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/trackers/riskctrl.py +0 -0
  134. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/trackers/sizers.py +0 -0
  135. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/__init__.py +0 -0
  136. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/_pyxreloader.py +0 -0
  137. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/charting/lookinglass.py +0 -0
  138. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  139. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/collections.py +0 -0
  140. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/marketdata/binance.py +0 -0
  141. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/marketdata/ccxt.py +0 -0
  142. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/marketdata/dukas.py +0 -0
  143. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/misc.py +0 -0
  144. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/ntp.py +0 -0
  145. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/numbers_utils.py +0 -0
  146. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/orderbook.py +0 -0
  147. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/plotting/__init__.py +0 -0
  148. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/plotting/dashboard.py +0 -0
  149. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/plotting/data.py +0 -0
  150. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/plotting/interfaces.py +0 -0
  151. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  152. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  153. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/questdb.py +0 -0
  154. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/runner/__init__.py +0 -0
  155. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/runner/accounts.py +0 -0
  156. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/runner/configs.py +0 -0
  157. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/runner/factory.py +0 -0
  158. {qubx-0.6.35 → qubx-0.6.37}/src/qubx/utils/time.py +0 -0
  159. {qubx-0.6.35 → qubx-0.6.37}/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.35
3
+ Version: 0.6.37
4
4
  Summary: Qubx - Quantitative Trading Framework
5
5
  Author: Dmitry Marienko
6
6
  Author-email: dmitry.marienko@xlydian.com
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "Qubx"
7
- version = "0.6.35"
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
- # Create and send an error event through the channel
97
- error_event = OrderCreationError(
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
- self.channel.send(create_error_event(error_event))
107
- return None
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
- error_event = OrderCreationError(
114
- timestamp=self.time_provider.time(),
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
- logger.error(f"Error in send_order: {err}")
179
- raise
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=False),
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
- await self.bfx.wss.inputs.submit_order(
102
- type=request["type"],
103
- symbol=request["symbol"],
104
- amount=float(request["amount"]),
105
- price=float(request["price"]),
106
- flags=request["flags"],
107
- # cid=int(request["cid"]),
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._strategy_state.is_warmup_in_progress:
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 is_simulated_trading(self) -> bool:
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 is_simulated_trading(self) -> bool:
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
- "exchange": self.exchanges,
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
- portfolio = pd.read_csv(zip_ref.open("portfolio.csv"), index_col=["timestamp"], parse_dates=["timestamp"])
859
- executions = pd.read_csv(zip_ref.open("executions.csv"), index_col=["timestamp"], parse_dates=["timestamp"])
860
- signals = pd.read_csv(zip_ref.open("signals.csv"), index_col=["timestamp"], parse_dates=["timestamp"])
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, deque
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, Union
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
- _start, _stop = handle_start_stop(start, stop, lambda x: pd.Timestamp(x).floor("d"))
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, _start, _stop, chunk_args)
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
- ) -> None:
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
- return quote_index, trade_index, orderbook_index
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