Qubx 0.6.36__tar.gz → 0.6.38__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 (164) hide show
  1. {qubx-0.6.36 → qubx-0.6.38}/PKG-INFO +1 -1
  2. {qubx-0.6.36 → qubx-0.6.38}/pyproject.toml +2 -1
  3. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/backtester/runner.py +2 -1
  4. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/connectors/ccxt/broker.py +68 -44
  5. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/connectors/ccxt/exchanges/__init__.py +1 -1
  6. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +7 -2
  7. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/core/context.py +13 -2
  8. qubx-0.6.38/src/qubx/core/errors.py +51 -0
  9. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/core/interfaces.py +11 -2
  10. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/core/loggers.py +3 -160
  11. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/core/metrics.py +26 -4
  12. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/data/hft.py +37 -9
  13. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/data/readers.py +22 -22
  14. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/features/core.py +8 -7
  15. qubx-0.6.38/src/qubx/loggers/__init__.py +17 -0
  16. qubx-0.6.38/src/qubx/loggers/csv.py +100 -0
  17. qubx-0.6.38/src/qubx/loggers/factory.py +55 -0
  18. qubx-0.6.38/src/qubx/loggers/inmemory.py +68 -0
  19. qubx-0.6.38/src/qubx/loggers/mongo.py +80 -0
  20. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/restorers/balance.py +76 -0
  21. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/restorers/factory.py +8 -4
  22. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/restorers/position.py +95 -0
  23. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/restorers/signal.py +115 -0
  24. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/restorers/state.py +89 -3
  25. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/utils/runner/_jupyter_runner.pyt +8 -1
  26. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/utils/runner/runner.py +6 -8
  27. qubx-0.6.36/src/qubx/core/errors.py +0 -32
  28. {qubx-0.6.36 → qubx-0.6.38}/LICENSE +0 -0
  29. {qubx-0.6.36 → qubx-0.6.38}/README.md +0 -0
  30. {qubx-0.6.36 → qubx-0.6.38}/build.py +0 -0
  31. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/__init__.py +0 -0
  32. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/_nb_magic.py +0 -0
  33. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/backtester/__init__.py +0 -0
  34. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/backtester/account.py +0 -0
  35. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/backtester/broker.py +0 -0
  36. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/backtester/data.py +0 -0
  37. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/backtester/management.py +0 -0
  38. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/backtester/ome.py +0 -0
  39. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/backtester/optimization.py +0 -0
  40. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/backtester/simulated_data.py +0 -0
  41. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/backtester/simulated_exchange.py +0 -0
  42. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/backtester/simulator.py +0 -0
  43. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/backtester/utils.py +0 -0
  44. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/cli/__init__.py +0 -0
  45. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/cli/commands.py +0 -0
  46. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/cli/deploy.py +0 -0
  47. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/cli/misc.py +0 -0
  48. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/cli/release.py +0 -0
  49. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/connectors/ccxt/__init__.py +0 -0
  50. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/connectors/ccxt/account.py +0 -0
  51. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/connectors/ccxt/data.py +0 -0
  52. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  53. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  54. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  55. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  56. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  57. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/connectors/ccxt/factory.py +0 -0
  58. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/connectors/ccxt/reader.py +0 -0
  59. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/connectors/ccxt/utils.py +0 -0
  60. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/connectors/tardis/data.py +0 -0
  61. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/connectors/tardis/utils.py +0 -0
  62. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/core/__init__.py +0 -0
  63. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/core/account.py +0 -0
  64. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/core/basics.py +0 -0
  65. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/core/deque.py +0 -0
  66. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/core/exceptions.py +0 -0
  67. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/core/helpers.py +0 -0
  68. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/core/initializer.py +0 -0
  69. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/core/lookups.py +0 -0
  70. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/core/mixins/__init__.py +0 -0
  71. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/core/mixins/market.py +0 -0
  72. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/core/mixins/processing.py +0 -0
  73. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/core/mixins/subscription.py +0 -0
  74. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/core/mixins/trading.py +0 -0
  75. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/core/mixins/universe.py +0 -0
  76. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/core/series.pxd +0 -0
  77. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/core/series.pyi +0 -0
  78. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/core/series.pyx +0 -0
  79. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/core/utils.pyi +0 -0
  80. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/core/utils.pyx +0 -0
  81. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/data/__init__.py +0 -0
  82. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/data/composite.py +0 -0
  83. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/data/helpers.py +0 -0
  84. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/data/registry.py +0 -0
  85. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/data/tardis.py +0 -0
  86. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/emitters/__init__.py +0 -0
  87. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/emitters/base.py +0 -0
  88. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/emitters/composite.py +0 -0
  89. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/emitters/csv.py +0 -0
  90. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/emitters/prometheus.py +0 -0
  91. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/emitters/questdb.py +0 -0
  92. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/exporters/__init__.py +0 -0
  93. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/exporters/composite.py +0 -0
  94. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/exporters/formatters/__init__.py +0 -0
  95. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/exporters/formatters/base.py +0 -0
  96. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/exporters/formatters/incremental.py +0 -0
  97. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/exporters/formatters/slack.py +0 -0
  98. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/exporters/redis_streams.py +0 -0
  99. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/exporters/slack.py +0 -0
  100. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/features/__init__.py +0 -0
  101. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/features/orderbook.py +0 -0
  102. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/features/price.py +0 -0
  103. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/features/trades.py +0 -0
  104. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/features/utils.py +0 -0
  105. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/gathering/simplest.py +0 -0
  106. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/health/__init__.py +0 -0
  107. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/health/base.py +0 -0
  108. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/math/__init__.py +0 -0
  109. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/math/stats.py +0 -0
  110. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/notifications/__init__.py +0 -0
  111. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/notifications/composite.py +0 -0
  112. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/notifications/slack.py +0 -0
  113. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/pandaz/__init__.py +0 -0
  114. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/pandaz/ta.py +0 -0
  115. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/pandaz/utils.py +0 -0
  116. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/resources/_build.py +0 -0
  117. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/resources/instruments/symbols-binance.cm.json +0 -0
  118. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/resources/instruments/symbols-binance.json +0 -0
  119. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/resources/instruments/symbols-binance.um.json +0 -0
  120. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/resources/instruments/symbols-bitfinex.f.json +0 -0
  121. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/resources/instruments/symbols-bitfinex.json +0 -0
  122. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/resources/instruments/symbols-kraken.f.json +0 -0
  123. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/resources/instruments/symbols-kraken.json +0 -0
  124. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/restarts/__init__.py +0 -0
  125. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/restarts/state_resolvers.py +0 -0
  126. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/restarts/time_finders.py +0 -0
  127. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/restorers/__init__.py +0 -0
  128. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/restorers/interfaces.py +0 -0
  129. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/restorers/utils.py +0 -0
  130. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/ta/__init__.py +0 -0
  131. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/ta/indicators.pxd +0 -0
  132. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/ta/indicators.pyi +0 -0
  133. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/ta/indicators.pyx +0 -0
  134. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/trackers/__init__.py +0 -0
  135. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/trackers/advanced.py +0 -0
  136. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/trackers/composite.py +0 -0
  137. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/trackers/rebalancers.py +0 -0
  138. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/trackers/riskctrl.py +0 -0
  139. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/trackers/sizers.py +0 -0
  140. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/utils/__init__.py +0 -0
  141. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/utils/_pyxreloader.py +0 -0
  142. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/utils/charting/lookinglass.py +0 -0
  143. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  144. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/utils/collections.py +0 -0
  145. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/utils/marketdata/binance.py +0 -0
  146. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/utils/marketdata/ccxt.py +0 -0
  147. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/utils/marketdata/dukas.py +0 -0
  148. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/utils/misc.py +0 -0
  149. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/utils/ntp.py +0 -0
  150. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/utils/numbers_utils.py +0 -0
  151. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/utils/orderbook.py +0 -0
  152. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/utils/plotting/__init__.py +0 -0
  153. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/utils/plotting/dashboard.py +0 -0
  154. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/utils/plotting/data.py +0 -0
  155. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/utils/plotting/interfaces.py +0 -0
  156. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  157. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  158. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/utils/questdb.py +0 -0
  159. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/utils/runner/__init__.py +0 -0
  160. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/utils/runner/accounts.py +0 -0
  161. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/utils/runner/configs.py +0 -0
  162. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/utils/runner/factory.py +0 -0
  163. {qubx-0.6.36 → qubx-0.6.38}/src/qubx/utils/time.py +0 -0
  164. {qubx-0.6.36 → qubx-0.6.38}/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.36
3
+ Version: 0.6.38
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.36"
7
+ version = "0.6.38"
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"
@@ -115,6 +115,7 @@ pytest-asyncio = "^0.24.0"
115
115
  pytest-mock = "^3.12.0"
116
116
  pytest-lazy-fixture = "^0.6.3"
117
117
  pytest-cov = "^4.1.0"
118
+ mongomock = "^4.3.0"
118
119
 
119
120
  [tool.poetry.group.k8.dependencies]
120
121
  prometheus-client = "^0.21.1"
@@ -20,8 +20,9 @@ from qubx.core.interfaces import (
20
20
  ITimeProvider,
21
21
  StrategyState,
22
22
  )
23
- from qubx.core.loggers import InMemoryLogsWriter, StrategyLogging
23
+ from qubx.core.loggers import StrategyLogging
24
24
  from qubx.core.lookups import lookup
25
+ from qubx.loggers.inmemory import InMemoryLogsWriter
25
26
  from qubx.pandaz.utils import _frame_to_str
26
27
 
27
28
  from .account import SimulatedAccountProcessor
@@ -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
 
@@ -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
 
@@ -1,10 +1,6 @@
1
- import csv
2
- import os
3
- from multiprocessing.pool import ThreadPool
4
1
  from typing import Any, Dict, List, Tuple
5
2
 
6
3
  import numpy as np
7
- import pandas as pd
8
4
 
9
5
  from qubx import logger
10
6
  from qubx.core.basics import (
@@ -14,11 +10,11 @@ from qubx.core.basics import (
14
10
  Position,
15
11
  TargetPosition,
16
12
  )
17
- from qubx.core.metrics import split_cumulative_pnl
13
+
18
14
  from qubx.core.series import time_as_nsec
19
15
  from qubx.core.utils import recognize_timeframe
20
- from qubx.pandaz.utils import scols
21
- from qubx.utils.misc import Stopwatch, makedirs
16
+
17
+ from qubx.utils.misc import Stopwatch
22
18
  from qubx.utils.time import convert_tf_str_td64, floor_t64
23
19
 
24
20
  _SW = Stopwatch()
@@ -48,159 +44,6 @@ class LogsWriter:
48
44
  pass
49
45
 
50
46
 
51
- class InMemoryLogsWriter(LogsWriter):
52
- _portfolio: List
53
- _execs: List
54
- _signals: List
55
-
56
- def __init__(self, account_id: str, strategy_id: str, run_id: str) -> None:
57
- super().__init__(account_id, strategy_id, run_id)
58
- self._portfolio = []
59
- self._execs = []
60
- self._signals = []
61
-
62
- def write_data(self, log_type: str, data: List[Dict[str, Any]]):
63
- if len(data) > 0:
64
- if log_type == "portfolio":
65
- self._portfolio.extend(data)
66
- elif log_type == "executions":
67
- self._execs.extend(data)
68
- elif log_type == "signals":
69
- self._signals.extend(data)
70
-
71
- def get_portfolio(self, as_plain_dataframe=True) -> pd.DataFrame:
72
- pfl = pd.DataFrame.from_records(self._portfolio, index="timestamp")
73
- pfl.index = pd.DatetimeIndex(pfl.index)
74
- if as_plain_dataframe:
75
- # - convert to Qube presentation (TODO: temporary)
76
- pis = []
77
- for s in set(pfl["symbol"]):
78
- pi = pfl[pfl["symbol"] == s]
79
- pi = pi.drop(columns=["symbol", "realized_pnl_quoted", "current_price", "exchange_time"])
80
- pi = pi.rename(
81
- {
82
- "pnl_quoted": "PnL",
83
- "quantity": "Pos",
84
- "avg_position_price": "Price",
85
- "market_value_quoted": "Value",
86
- "commissions_quoted": "Commissions",
87
- },
88
- axis=1,
89
- )
90
- # We want to convert the value to just price * quantity
91
- # in reality value of perps is just the unrealized pnl but
92
- # it's not important after simulation for metric calculations
93
- pi["Value"] = pi["Pos"] * pi["Price"] + pi["Value"]
94
- pis.append(pi.rename(lambda x: s + "_" + x, axis=1))
95
- return split_cumulative_pnl(scols(*pis))
96
- return pfl
97
-
98
- def get_executions(self) -> pd.DataFrame:
99
- p = pd.DataFrame()
100
- if self._execs:
101
- p = pd.DataFrame.from_records(self._execs, index="timestamp")
102
- p.index = pd.DatetimeIndex(p.index)
103
- return p
104
-
105
- def get_signals(self) -> pd.DataFrame:
106
- p = pd.DataFrame()
107
- if self._signals:
108
- p = pd.DataFrame.from_records(self._signals, index="timestamp")
109
- p.index = pd.DatetimeIndex(p.index)
110
- return p
111
-
112
-
113
- class CsvFileLogsWriter(LogsWriter):
114
- """
115
- Simple CSV strategy log data writer. It does data writing in separate thread.
116
- """
117
-
118
- def __init__(self, account_id: str, strategy_id: str, run_id: str, log_folder="logs") -> None:
119
- super().__init__(account_id, strategy_id, run_id)
120
-
121
- path = makedirs(log_folder)
122
- # - it rewrites positions every time
123
- self._pos_file_path = f"{path}/{self.strategy_id}_{self.account_id}_positions.csv"
124
- self._balance_file_path = f"{path}/{self.strategy_id}_{self.account_id}_balance.csv"
125
-
126
- _pfl_path = f"{path}/{strategy_id}_{account_id}_portfolio.csv"
127
- _exe_path = f"{path}/{strategy_id}_{account_id}_executions.csv"
128
- _sig_path = f"{path}/{strategy_id}_{account_id}_signals.csv"
129
- self._hdr_pfl = not os.path.exists(_pfl_path)
130
- self._hdr_exe = not os.path.exists(_exe_path)
131
- self._hdr_sig = not os.path.exists(_sig_path)
132
-
133
- self._pfl_file_ = open(_pfl_path, "+a", newline="")
134
- self._execs_file_ = open(_exe_path, "+a", newline="")
135
- self._sig_file_ = open(_sig_path, "+a", newline="")
136
- self._pfl_writer = csv.writer(self._pfl_file_)
137
- self._exe_writer = csv.writer(self._execs_file_)
138
- self._sig_writer = csv.writer(self._sig_file_)
139
- self.pool = ThreadPool(3)
140
-
141
- @staticmethod
142
- def _header(d: dict) -> List[str]:
143
- return list(d.keys()) + ["run_id"]
144
-
145
- def _values(self, data: List[Dict[str, Any]]) -> List[List[str]]:
146
- # - attach run_id (last column)
147
- return [list((d | {"run_id": self.run_id}).values()) for d in data]
148
-
149
- def _do_write(self, log_type, data):
150
- match log_type:
151
- case "positions":
152
- with open(self._pos_file_path, "w", newline="") as f:
153
- w = csv.writer(f)
154
- w.writerow(self._header(data[0]))
155
- w.writerows(self._values(data))
156
-
157
- case "portfolio":
158
- if self._hdr_pfl:
159
- self._pfl_writer.writerow(self._header(data[0]))
160
- self._hdr_pfl = False
161
- self._pfl_writer.writerows(self._values(data))
162
- self._pfl_file_.flush()
163
-
164
- case "executions":
165
- if self._hdr_exe:
166
- self._exe_writer.writerow(self._header(data[0]))
167
- self._hdr_exe = False
168
- self._exe_writer.writerows(self._values(data))
169
- self._execs_file_.flush()
170
-
171
- case "signals":
172
- if self._hdr_sig:
173
- self._sig_writer.writerow(self._header(data[0]))
174
- self._hdr_sig = False
175
- self._sig_writer.writerows(self._values(data))
176
- self._sig_file_.flush()
177
-
178
- case "balance":
179
- with open(self._balance_file_path, "w", newline="") as f:
180
- w = csv.writer(f)
181
- w.writerow(self._header(data[0]))
182
- w.writerows(self._values(data))
183
-
184
- def write_data(self, log_type: str, data: List[Dict[str, Any]]):
185
- if len(data) > 0:
186
- self.pool.apply_async(self._do_write, (log_type, data))
187
-
188
- def flush_data(self):
189
- try:
190
- self._pfl_file_.flush()
191
- self._execs_file_.flush()
192
- self._sig_file_.flush()
193
- except Exception as e:
194
- logger.warning(f"Error flushing log writer: {str(e)}")
195
-
196
- def close(self):
197
- self._pfl_file_.close()
198
- self._execs_file_.close()
199
- self._sig_file_.close()
200
- self.pool.close()
201
- self.pool.join()
202
-
203
-
204
47
  class _BaseIntervalDumper:
205
48
  """
206
49
  Basic functionality for all interval based dumpers
@@ -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