Qubx 0.6.65__tar.gz → 0.6.66__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 (175) hide show
  1. {qubx-0.6.65 → qubx-0.6.66}/PKG-INFO +1 -1
  2. {qubx-0.6.65 → qubx-0.6.66}/pyproject.toml +1 -1
  3. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/backtester/broker.py +9 -2
  4. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/backtester/runner.py +112 -12
  5. qubx-0.6.66/src/qubx/backtester/sentinels.py +23 -0
  6. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/backtester/simulated_data.py +12 -0
  7. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/backtester/simulated_exchange.py +6 -3
  8. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/backtester/simulator.py +14 -6
  9. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/backtester/utils.py +25 -5
  10. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/connectors/ccxt/data.py +1 -1
  11. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/connectors/ccxt/reader.py +4 -4
  12. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/connectors/ccxt/utils.py +3 -3
  13. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/core/basics.py +18 -23
  14. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/core/context.py +24 -0
  15. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/core/helpers.py +21 -4
  16. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/core/initializer.py +86 -1
  17. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/core/interfaces.py +82 -0
  18. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/core/mixins/processing.py +88 -1
  19. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/core/mixins/trading.py +34 -4
  20. qubx-0.6.66/src/qubx/core/stale_data_detector.py +418 -0
  21. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/data/__init__.py +2 -1
  22. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/data/composite.py +7 -4
  23. qubx-0.6.66/src/qubx/data/helpers.py +2054 -0
  24. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/data/readers.py +3 -5
  25. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/emitters/base.py +1 -1
  26. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/gathering/simplest.py +3 -1
  27. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/restarts/state_resolvers.py +5 -1
  28. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/trackers/riskctrl.py +13 -2
  29. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/utils/runner/_jupyter_runner.pyt +9 -2
  30. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/utils/runner/configs.py +11 -0
  31. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/utils/runner/runner.py +7 -0
  32. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/utils/time.py +56 -0
  33. qubx-0.6.65/src/qubx/data/helpers.py +0 -443
  34. {qubx-0.6.65 → qubx-0.6.66}/LICENSE +0 -0
  35. {qubx-0.6.65 → qubx-0.6.66}/README.md +0 -0
  36. {qubx-0.6.65 → qubx-0.6.66}/build.py +0 -0
  37. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/__init__.py +0 -0
  38. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/_nb_magic.py +0 -0
  39. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/backtester/__init__.py +0 -0
  40. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/backtester/account.py +0 -0
  41. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/backtester/data.py +0 -0
  42. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/backtester/management.py +0 -0
  43. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/backtester/ome.py +0 -0
  44. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/backtester/optimization.py +0 -0
  45. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/cli/__init__.py +0 -0
  46. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/cli/commands.py +0 -0
  47. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/cli/deploy.py +0 -0
  48. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/cli/misc.py +0 -0
  49. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/cli/release.py +0 -0
  50. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/cli/tui.py +0 -0
  51. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/connectors/ccxt/__init__.py +0 -0
  52. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/connectors/ccxt/account.py +0 -0
  53. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/connectors/ccxt/broker.py +0 -0
  54. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  55. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
  56. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  57. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
  58. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  59. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  60. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  61. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/connectors/ccxt/factory.py +0 -0
  62. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/connectors/tardis/data.py +0 -0
  63. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/connectors/tardis/utils.py +0 -0
  64. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/core/__init__.py +0 -0
  65. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/core/account.py +0 -0
  66. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/core/deque.py +0 -0
  67. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/core/errors.py +0 -0
  68. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/core/exceptions.py +0 -0
  69. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/core/loggers.py +0 -0
  70. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/core/lookups.py +0 -0
  71. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/core/metrics.py +0 -0
  72. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/core/mixins/__init__.py +0 -0
  73. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/core/mixins/market.py +0 -0
  74. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/core/mixins/subscription.py +0 -0
  75. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/core/mixins/universe.py +0 -0
  76. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/core/series.pxd +0 -0
  77. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/core/series.pyi +0 -0
  78. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/core/series.pyx +0 -0
  79. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/core/utils.pyi +0 -0
  80. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/core/utils.pyx +0 -0
  81. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/data/hft.py +0 -0
  82. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/data/registry.py +0 -0
  83. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/data/tardis.py +0 -0
  84. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/emitters/__init__.py +0 -0
  85. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/emitters/composite.py +0 -0
  86. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/emitters/csv.py +0 -0
  87. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/emitters/indicator.py +0 -0
  88. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/emitters/inmemory.py +0 -0
  89. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/emitters/prometheus.py +0 -0
  90. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/emitters/questdb.py +0 -0
  91. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/exporters/__init__.py +0 -0
  92. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/exporters/composite.py +0 -0
  93. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/exporters/formatters/__init__.py +0 -0
  94. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/exporters/formatters/base.py +0 -0
  95. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/exporters/formatters/incremental.py +0 -0
  96. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/exporters/formatters/slack.py +0 -0
  97. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/exporters/redis_streams.py +0 -0
  98. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/exporters/slack.py +0 -0
  99. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/features/__init__.py +0 -0
  100. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/features/core.py +0 -0
  101. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/features/orderbook.py +0 -0
  102. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/features/price.py +0 -0
  103. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/features/trades.py +0 -0
  104. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/features/utils.py +0 -0
  105. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/health/__init__.py +0 -0
  106. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/health/base.py +0 -0
  107. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/loggers/__init__.py +0 -0
  108. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/loggers/csv.py +0 -0
  109. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/loggers/factory.py +0 -0
  110. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/loggers/inmemory.py +0 -0
  111. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/loggers/mongo.py +0 -0
  112. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/math/__init__.py +0 -0
  113. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/math/stats.py +0 -0
  114. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/notifications/__init__.py +0 -0
  115. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/notifications/composite.py +0 -0
  116. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/notifications/slack.py +0 -0
  117. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/notifications/throttler.py +0 -0
  118. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/pandaz/__init__.py +0 -0
  119. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/pandaz/ta.py +0 -0
  120. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/pandaz/utils.py +0 -0
  121. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/resources/_build.py +0 -0
  122. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/resources/crypto-fees.ini +0 -0
  123. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  124. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  125. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  126. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  127. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  128. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  129. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/resources/instruments/symbols-hyperliquid-spot.json +0 -0
  130. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/resources/instruments/symbols-hyperliquid.f-perpetual.json +0 -0
  131. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  132. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  133. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  134. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/restarts/__init__.py +0 -0
  135. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/restarts/time_finders.py +0 -0
  136. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/restorers/__init__.py +0 -0
  137. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/restorers/balance.py +0 -0
  138. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/restorers/factory.py +0 -0
  139. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/restorers/interfaces.py +0 -0
  140. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/restorers/position.py +0 -0
  141. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/restorers/signal.py +0 -0
  142. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/restorers/state.py +0 -0
  143. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/restorers/utils.py +0 -0
  144. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/ta/__init__.py +0 -0
  145. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/ta/indicators.pxd +0 -0
  146. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/ta/indicators.pyi +0 -0
  147. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/ta/indicators.pyx +0 -0
  148. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/trackers/__init__.py +0 -0
  149. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/trackers/advanced.py +0 -0
  150. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/trackers/composite.py +0 -0
  151. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/trackers/rebalancers.py +0 -0
  152. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/trackers/sizers.py +0 -0
  153. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/utils/__init__.py +0 -0
  154. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/utils/_pyxreloader.py +0 -0
  155. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/utils/charting/lookinglass.py +0 -0
  156. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  157. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/utils/collections.py +0 -0
  158. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/utils/marketdata/binance.py +0 -0
  159. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/utils/marketdata/ccxt.py +0 -0
  160. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/utils/marketdata/dukas.py +0 -0
  161. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/utils/misc.py +0 -0
  162. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/utils/ntp.py +0 -0
  163. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/utils/numbers_utils.py +0 -0
  164. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/utils/orderbook.py +0 -0
  165. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/utils/plotting/__init__.py +0 -0
  166. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/utils/plotting/dashboard.py +0 -0
  167. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/utils/plotting/data.py +0 -0
  168. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/utils/plotting/interfaces.py +0 -0
  169. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  170. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  171. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/utils/questdb.py +0 -0
  172. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/utils/runner/__init__.py +0 -0
  173. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/utils/runner/accounts.py +0 -0
  174. {qubx-0.6.65 → qubx-0.6.66}/src/qubx/utils/runner/factory.py +0 -0
  175. {qubx-0.6.65 → qubx-0.6.66}/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.65
3
+ Version: 0.6.66
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.65"
7
+ version = "0.6.66"
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"
@@ -1,3 +1,4 @@
1
+ from qubx import logger
1
2
  from qubx.backtester.ome import SimulatedExecutionReport
2
3
  from qubx.backtester.simulated_exchange import ISimulatedExchange
3
4
  from qubx.core.basics import (
@@ -5,6 +6,7 @@ from qubx.core.basics import (
5
6
  Instrument,
6
7
  Order,
7
8
  )
9
+ from qubx.core.exceptions import OrderNotFound
8
10
  from qubx.core.interfaces import IBroker
9
11
 
10
12
  from .account import SimulatedAccountProcessor
@@ -63,8 +65,13 @@ class SimulatedBroker(IBroker):
63
65
  self.send_order(instrument, order_side, order_type, amount, price, client_id, time_in_force, **optional)
64
66
 
65
67
  def cancel_order(self, order_id: str) -> Order | None:
66
- self._send_execution_report(order_update := self._exchange.cancel_order(order_id))
67
- return order_update.order if order_update is not None else None
68
+ try:
69
+ self._send_execution_report(order_update := self._exchange.cancel_order(order_id))
70
+ return order_update.order if order_update is not None else None
71
+ except OrderNotFound:
72
+ # Order was already cancelled or doesn't exist
73
+ logger.debug(f"Order {order_id} not found")
74
+ return None
68
75
 
69
76
  def cancel_orders(self, instrument: Instrument) -> None:
70
77
  raise NotImplementedError("Not implemented yet")
@@ -5,7 +5,9 @@ import pandas as pd
5
5
  from tqdm.auto import tqdm
6
6
 
7
7
  from qubx import logger
8
+ from qubx.backtester.sentinels import NoDataContinue
8
9
  from qubx.backtester.simulated_data import IterableSimulationData
10
+ from qubx.backtester.utils import SimulationDataConfig, TimeGuardedWrapper
9
11
  from qubx.core.account import CompositeAccountProcessor
10
12
  from qubx.core.basics import SW, DataType, Instrument, TransactionCostsCalculator
11
13
  from qubx.core.context import StrategyContext
@@ -22,8 +24,10 @@ from qubx.core.interfaces import (
22
24
  )
23
25
  from qubx.core.loggers import StrategyLogging
24
26
  from qubx.core.lookups import lookup
27
+ from qubx.data.helpers import CachedPrefetchReader
25
28
  from qubx.loggers.inmemory import InMemoryLogsWriter
26
29
  from qubx.pandaz.utils import _frame_to_str
30
+ from qubx.utils.time import now_ns
27
31
 
28
32
  from .account import SimulatedAccountProcessor
29
33
  from .broker import SimulatedBroker
@@ -80,6 +84,7 @@ class SimulationRunner:
80
84
  emitter: IMetricEmitter | None = None,
81
85
  strategy_state: StrategyState | None = None,
82
86
  initializer: BasicStrategyInitializer | None = None,
87
+ warmup_mode: bool = False,
83
88
  ):
84
89
  """
85
90
  Initialize the BacktestContextRunner with a strategy context.
@@ -102,8 +107,10 @@ class SimulationRunner:
102
107
  self.emitter = emitter
103
108
  self.strategy_state = strategy_state if strategy_state is not None else StrategyState()
104
109
  self.initializer = initializer
110
+ self.warmup_mode = warmup_mode
105
111
  self._pregenerated_signals = dict()
106
112
  self._to_process = {}
113
+ self._aux_data_reader = None
107
114
 
108
115
  # - get strategy parameters BEFORE simulation start
109
116
  # potentially strategy may change it's parameters during simulation
@@ -126,6 +133,8 @@ class SimulationRunner:
126
133
  """
127
134
  logger.debug(f"[<y>SimulationRunner</y>] :: Running simulation from {self.start} to {self.stop}")
128
135
 
136
+ self._prefetch_aux_data()
137
+
129
138
  # Start the context
130
139
  self.ctx.start()
131
140
 
@@ -164,6 +173,8 @@ class SimulationRunner:
164
173
  logger.error("Simulated trading interrupted by user!")
165
174
  if not catch_keyboard_interrupt:
166
175
  raise
176
+ except Exception as e:
177
+ raise e
167
178
  finally:
168
179
  # Stop the context
169
180
  self.ctx.stop()
@@ -196,7 +207,7 @@ class SimulationRunner:
196
207
  for i in self._data_providers[0].get_subscribed_instruments():
197
208
  # - we can process series with variable id's if we can find some similar instrument
198
209
  if s == i.symbol or s == str(i) or s == f"{i.exchange}:{i.symbol}" or str(s) == str(i):
199
- _start, _end = pd.Timestamp(start), pd.Timestamp(end)
210
+ _start, _end = np.datetime64(start), np.datetime64(end)
200
211
  _start_idx, _end_idx = v.index.get_indexer([_start, _end], method="ffill")
201
212
  sel = v.iloc[max(_start_idx, 0) : _end_idx + 1]
202
213
 
@@ -268,21 +279,34 @@ class SimulationRunner:
268
279
  start, stop = pd.Timestamp(start), pd.Timestamp(stop)
269
280
  total_duration = stop - start
270
281
  update_delta = total_duration / 100
271
- prev_dt = pd.Timestamp(start)
282
+ prev_dt = np.datetime64(start)
272
283
 
273
284
  # - date iteration
274
285
  qiter = self._data_source.create_iterable(start, stop)
286
+
275
287
  if silent:
276
288
  for instrument, data_type, event, is_hist in qiter:
277
- if not _run(instrument, data_type, event, is_hist):
289
+ # Handle NoDataContinue sentinel
290
+ if isinstance(event, NoDataContinue):
291
+ if not self._handle_no_data_scenario(stop):
292
+ break
293
+ continue
294
+
295
+ if not self._process_event(instrument, data_type, event, is_hist, _run, stop):
278
296
  break
279
297
  else:
280
298
  _p = 0
281
299
  with tqdm(total=100, desc="Simulating", unit="%", leave=False) as pbar:
282
300
  for instrument, data_type, event, is_hist in qiter:
283
- if not _run(instrument, data_type, event, is_hist):
301
+ # Handle NoDataContinue sentinel
302
+ if isinstance(event, NoDataContinue):
303
+ if not self._handle_no_data_scenario(stop):
304
+ break
305
+ continue
306
+
307
+ if not self._process_event(instrument, data_type, event, is_hist, _run, stop):
284
308
  break
285
- dt = pd.Timestamp(event.time)
309
+ dt = np.datetime64(event.time, "ns")
286
310
  # update only if date has changed
287
311
  if dt - prev_dt > update_delta:
288
312
  _p += 1
@@ -294,6 +318,43 @@ class SimulationRunner:
294
318
 
295
319
  logger.info(f"{self.__class__.__name__} ::: Simulation finished at {stop} :::")
296
320
 
321
+ def _process_event(self, instrument, data_type, event, is_hist, _run, stop_time):
322
+ """Process a single simulation event with proper time advancement and scheduler checks."""
323
+ # During warmup, clamp future timestamps to current time
324
+ if self.warmup_mode and hasattr(event, "time"):
325
+ current_real_time = now_ns()
326
+ if event.time > current_real_time:
327
+ event.time = current_real_time
328
+
329
+ if not _run(instrument, data_type, event, is_hist):
330
+ return False
331
+ return True
332
+
333
+ def _handle_no_data_scenario(self, stop_time):
334
+ """Handle scenario when no data is available but scheduler might have events."""
335
+ # Check if we have pending scheduled events
336
+ if hasattr(self.scheduler, "_next_nearest_time"):
337
+ next_scheduled_time = self.scheduler._next_nearest_time
338
+ current_time = self.time_provider.time()
339
+
340
+ # Convert to int64 for numerical comparisons (avoid type issues)
341
+ next_time_ns = next_scheduled_time.astype("int64")
342
+ current_time_ns = current_time.astype("int64")
343
+ stop_time_ns = stop_time.value # Already int64
344
+
345
+ # Check if we've reached the stop time
346
+ if current_time_ns >= stop_time_ns:
347
+ return False # Stop simulation
348
+
349
+ # If there's a scheduled event before stop time, advance to it
350
+ if next_time_ns < np.iinfo(np.int64).max and next_time_ns < stop_time_ns:
351
+ # Use the original datetime64 object for set_time (not the int64 conversion)
352
+ self.time_provider.set_time(next_scheduled_time)
353
+ self.scheduler.check_and_run_tasks()
354
+ return True # Continue simulation
355
+
356
+ return False # No scheduled events, stop simulation
357
+
297
358
  def print_latency_report(self) -> None:
298
359
  _l_r = SW.latency_report()
299
360
  if _l_r is not None:
@@ -311,11 +372,6 @@ class SimulationRunner:
311
372
  f"for {self.setup.capital} {self.setup.base_currency}..."
312
373
  )
313
374
 
314
- data_source = IterableSimulationData(
315
- self.data_config.data_providers,
316
- open_close_time_indent_secs=self.data_config.adjusted_open_close_time_indent_secs,
317
- )
318
-
319
375
  channel = SimulatedCtrlChannel("databus", sentinel=(None, None, None, None))
320
376
  simulated_clock = SimulatedTimeProvider(np.datetime64(self.start, "ns"))
321
377
 
@@ -325,6 +381,11 @@ class SimulationRunner:
325
381
 
326
382
  scheduler = SimulatedScheduler(channel, lambda: simulated_clock.time().item())
327
383
 
384
+ data_source = IterableSimulationData(
385
+ self.data_config.data_providers,
386
+ open_close_time_indent_secs=self.data_config.adjusted_open_close_time_indent_secs,
387
+ )
388
+
328
389
  brokers = []
329
390
  for exchange in self.setup.exchanges:
330
391
  _exchange_account = account.get_account_processor(exchange)
@@ -349,7 +410,7 @@ class SimulationRunner:
349
410
  )
350
411
 
351
412
  # - get aux data provider
352
- _aux_data = self.data_config.get_timeguarded_aux_reader(simulated_clock)
413
+ self._aux_data_reader = self.data_config.get_timeguarded_aux_reader(simulated_clock)
353
414
 
354
415
  # - it will store simulation results into memory
355
416
  logs_writer = InMemoryLogsWriter(self.account_id, self.setup.name, "0")
@@ -401,7 +462,7 @@ class SimulationRunner:
401
462
  time_provider=simulated_clock,
402
463
  instruments=self.setup.instruments,
403
464
  logging=StrategyLogging(logs_writer, portfolio_log_freq=self.portfolio_log_freq),
404
- aux_data_provider=_aux_data,
465
+ aux_data_provider=self._aux_data_reader,
405
466
  emitter=self.emitter,
406
467
  strategy_state=self.strategy_state,
407
468
  initializer=self.initializer,
@@ -487,3 +548,42 @@ class SimulationRunner:
487
548
  time_provider=time_provider,
488
549
  account_processors=_account_processors,
489
550
  )
551
+
552
+ def _prefetch_aux_data(self):
553
+ # Perform prefetch of aux data if enabled
554
+ if self._aux_data_reader is None:
555
+ return
556
+
557
+ aux_reader = self._aux_data_reader
558
+ if isinstance(aux_reader, TimeGuardedWrapper) and isinstance(aux_reader._reader, CachedPrefetchReader):
559
+ aux_reader = aux_reader._reader
560
+ elif isinstance(aux_reader, CachedPrefetchReader):
561
+ aux_reader = aux_reader
562
+ else:
563
+ return
564
+
565
+ if self.data_config.prefetch_config and self.data_config.prefetch_config.enabled:
566
+ # Prepare prefetch arguments
567
+ prefetch_args = self.data_config.prefetch_config.args.copy()
568
+
569
+ # Add exchange info if available from instruments
570
+ if self.setup.instruments and "exchange" not in prefetch_args:
571
+ # Get exchange from first instrument
572
+ first_exchange = self.setup.instruments[0].exchange
573
+ if first_exchange:
574
+ prefetch_args["exchange"] = first_exchange
575
+
576
+ logger.info(
577
+ f"Prefetching aux data: {self.data_config.prefetch_config.aux_data_names} for period {self.start} to {self.stop}"
578
+ )
579
+
580
+ try:
581
+ # Perform the prefetch
582
+ aux_reader.prefetch_aux_data(
583
+ self.data_config.prefetch_config.aux_data_names,
584
+ start=str(self.start),
585
+ stop=str(self.stop),
586
+ **prefetch_args,
587
+ )
588
+ except Exception as e:
589
+ logger.warning(f"Prefetch failed: {e}")
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+
6
+ class NoDataContinue:
7
+ """Sentinel indicating no data streams available but simulation should continue.
8
+
9
+ This is used when all instruments are unsubscribed but there may still be
10
+ scheduled events to process before the simulation stop time.
11
+ """
12
+
13
+ def __init__(self, next_scheduled_time: Optional[int] = None):
14
+ """Initialize the sentinel.
15
+
16
+ Args:
17
+ next_scheduled_time: The next scheduled event time in nanoseconds,
18
+ or None if no scheduled events exist.
19
+ """
20
+ self.next_scheduled_time = next_scheduled_time
21
+
22
+ def __repr__(self) -> str:
23
+ return f"NoDataContinue(next_scheduled_time={self.next_scheduled_time})"
@@ -3,6 +3,7 @@ from typing import Any, Iterator
3
3
  import pandas as pd
4
4
 
5
5
  from qubx import logger
6
+ from qubx.backtester.sentinels import NoDataContinue
6
7
  from qubx.core.basics import DataType, Instrument, MarketType, Timestamped
7
8
  from qubx.core.exceptions import SimulationError
8
9
  from qubx.data.composite import IteratedDataStreamsSlicer
@@ -447,6 +448,17 @@ class IterableSimulationData(Iterator):
447
448
  try:
448
449
  while data := next(self._slicing_iterator): # type: ignore
449
450
  k, t, v = data
451
+
452
+ # Check if we've reached or exceeded the stop time
453
+ # It's commented out because we expect data readers to stop on their own
454
+ # if self._stop is not None and t > self._stop.value:
455
+ # raise StopIteration
456
+
457
+ # Handle NoDataContinue sentinel
458
+ if isinstance(v, NoDataContinue):
459
+ # Return the sentinel as the event - the runner will detect it with isinstance
460
+ return None, "", v, False
461
+
450
462
  instr, fetcher, subt = self._instruments[k]
451
463
  data_type = fetcher._producing_data_type
452
464
  _is_historical = False
@@ -11,6 +11,7 @@ from qubx.core.basics import (
11
11
  TransactionCostsCalculator,
12
12
  dt_64,
13
13
  )
14
+ from qubx.core.exceptions import OrderNotFound
14
15
  from qubx.core.series import Bar, OrderBook, Quote, Trade, TradeArray
15
16
 
16
17
 
@@ -155,8 +156,7 @@ class BasicSimulatedExchange(ISimulatedExchange):
155
156
  if order.id == order_id:
156
157
  return self._process_ome_response(o.cancel_order(order_id))
157
158
 
158
- logger.warning(f"[<y>{self.__class__.__name__}</y>] :: cancel_order :: can't find order '{order_id}'!")
159
- return None
159
+ raise OrderNotFound(f"Order '{order_id}' not found")
160
160
 
161
161
  ome = self._ome.get(instrument)
162
162
  if ome is None:
@@ -165,7 +165,10 @@ class BasicSimulatedExchange(ISimulatedExchange):
165
165
  )
166
166
 
167
167
  # - cancel order in OME and remove from the map to free memory
168
- return self._process_ome_response(ome.cancel_order(order_id))
168
+ result = self._process_ome_response(ome.cancel_order(order_id))
169
+ if result is None:
170
+ raise OrderNotFound(f"Order '{order_id}' not found")
171
+ return result
169
172
 
170
173
  def _process_ome_response(self, report: SimulatedExecutionReport | None) -> SimulatedExecutionReport | None:
171
174
  if report is not None:
@@ -11,7 +11,8 @@ from qubx.core.metrics import TradingSessionResult
11
11
  from qubx.data.readers import DataReader
12
12
  from qubx.emitters.inmemory import InMemoryMetricEmitter
13
13
  from qubx.utils.misc import ProgressParallel, Stopwatch, get_current_user
14
- from qubx.utils.time import handle_start_stop
14
+ from qubx.utils.runner.configs import PrefetchConfig
15
+ from qubx.utils.time import handle_start_stop, to_utc_naive
15
16
 
16
17
  from .runner import SimulationRunner
17
18
  from .utils import (
@@ -32,10 +33,10 @@ def simulate(
32
33
  strategies: StrategiesDecls_t,
33
34
  data: DataDecls_t,
34
35
  capital: float | dict[str, float],
35
- instruments: list[str] | list[Instrument] | dict[ExchangeName_t, list[SymbolOrInstrument_t]],
36
- commissions: str | dict[str, str | None] | None,
37
36
  start: str | pd.Timestamp,
38
37
  stop: str | pd.Timestamp | None = None,
38
+ instruments: list[str] | list[Instrument] | dict[ExchangeName_t, list[SymbolOrInstrument_t]] | None = None,
39
+ commissions: str | dict[str, str | None] | None = None,
39
40
  exchange: ExchangeName_t | list[ExchangeName_t] | None = None,
40
41
  base_currency: str = "USDT",
41
42
  n_jobs: int = 1,
@@ -52,6 +53,7 @@ def simulate(
52
53
  enable_inmemory_emitter: bool = False,
53
54
  emitter_stats_interval: str = "1h",
54
55
  run_separate_instruments: bool = False,
56
+ prefetch_config: PrefetchConfig | None = None,
55
57
  ) -> list[TradingSessionResult]:
56
58
  """
57
59
  Backtest utility for trading strategies or signals using historical data.
@@ -80,6 +82,7 @@ def simulate(
80
82
  - enable_inmemory_emitter (bool): If True, attaches an in-memory metric emitter and returns its dataframe in TradingSessionResult.emitter_data.
81
83
  - emitter_stats_interval (str): Interval for emitting stats in the in-memory emitter (default: "1h").
82
84
  - run_separate_instruments (bool): If True, creates separate simulation setups for each instrument, default is False.
85
+ - prefetch_config (dict[str, Any] | None): Configuration for prefetching auxiliary data, default is None.
83
86
 
84
87
  Returns:
85
88
  - list[TradingSessionResult]: A list of TradingSessionResult objects containing the results of each simulation setup.
@@ -91,6 +94,9 @@ def simulate(
91
94
  # - we need to reset stopwatch
92
95
  Stopwatch().reset()
93
96
 
97
+ if instruments is None:
98
+ instruments = []
99
+
94
100
  # - process instruments:
95
101
  _instruments, _exchanges = find_instruments_and_exchanges(instruments, exchange)
96
102
 
@@ -103,7 +109,9 @@ def simulate(
103
109
  raise SimulationError(_msg)
104
110
 
105
111
  # - recognize provided data
106
- data_setup = recognize_simulation_data_config(data, _instruments, open_close_time_indent_secs, aux_data)
112
+ data_setup = recognize_simulation_data_config(
113
+ data, _instruments, open_close_time_indent_secs, aux_data, prefetch_config
114
+ )
107
115
 
108
116
  # - recognize setup: it can be either a strategy or set of signals
109
117
  simulation_setups = recognize_simulation_configuration(
@@ -133,7 +141,7 @@ def simulate(
133
141
  # - preprocess start and stop and convert to datetime if necessary
134
142
  if stop is None:
135
143
  # - check stop time : here we try to backtest till now (may be we need to get max available time from data reader ?)
136
- stop = pd.Timestamp.now(tz="UTC").astimezone(None)
144
+ stop = to_utc_naive(pd.Timestamp.now(tz="UTC"))
137
145
 
138
146
  _start, _stop = handle_start_stop(start, stop, convert=pd.Timestamp)
139
147
  assert isinstance(_start, pd.Timestamp) and isinstance(_stop, pd.Timestamp), "Invalid start and stop times"
@@ -229,7 +237,7 @@ def _adjust_start_date_for_min_instrument_onboard(setup: SimulationSetup, start:
229
237
  Adjust the start date for the simulation to the onboard date of the instrument with the minimum onboard date.
230
238
  """
231
239
  onboard_dates = [
232
- pd.Timestamp(instrument.onboard_date).replace(tzinfo=None)
240
+ to_utc_naive(pd.Timestamp(instrument.onboard_date))
233
241
  for instrument in setup.instruments
234
242
  if instrument.onboard_date is not None
235
243
  ]
@@ -24,9 +24,10 @@ from qubx.core.lookups import lookup
24
24
  from qubx.core.series import OHLCV, Bar, Quote, Trade
25
25
  from qubx.core.utils import time_delta_to_str
26
26
  from qubx.data import TardisMachineReader
27
- from qubx.data.helpers import InMemoryCachedReader, TimeGuardedWrapper
27
+ from qubx.data.helpers import CachedPrefetchReader, TimeGuardedWrapper
28
28
  from qubx.data.hft import HftDataReader
29
29
  from qubx.data.readers import AsDict, DataReader, InMemoryDataFrameReader
30
+ from qubx.utils.runner.configs import PrefetchConfig
30
31
  from qubx.utils.time import infer_series_frequency, timedelta_to_crontab
31
32
 
32
33
  SymbolOrInstrument_t: TypeAlias = str | Instrument
@@ -108,14 +109,31 @@ class SimulationDataConfig:
108
109
  default_warmups: dict[str, str] # default warmups periods
109
110
  open_close_time_indent_secs: int # open/close ticks shift in seconds
110
111
  adjusted_open_close_time_indent_secs: int # adjusted open/close ticks shift in seconds
111
- aux_data_provider: InMemoryCachedReader | None = None # auxiliary data provider
112
+ aux_data_provider: DataReader | None = None # auxiliary data provider
113
+ prefetch_config: PrefetchConfig | None = None # prefetch configuration
112
114
 
113
115
  def get_timeguarded_aux_reader(self, time_provider: ITimeProvider) -> TimeGuardedWrapper | None:
114
116
  _aux = None
115
117
  if self.aux_data_provider is not None:
116
- if not isinstance(self.aux_data_provider, InMemoryCachedReader):
117
- logger.warning("Aux data provider should be an instance of InMemoryCachedReader ! Otherwise it can lead to unnecessary effects !")
118
- _aux = TimeGuardedWrapper(self.aux_data_provider, time_guard=time_provider)
118
+ aux_reader = self.aux_data_provider
119
+
120
+ # Wrap with CachedPrefetchReader if not already wrapped
121
+ if not isinstance(aux_reader, CachedPrefetchReader):
122
+ prefetch_period = "1w"
123
+ cache_size_mb = 100
124
+
125
+ # Get prefetch configuration if available
126
+ if self.prefetch_config:
127
+ prefetch_period = self.prefetch_config.prefetch_period
128
+ cache_size_mb = self.prefetch_config.cache_size_mb
129
+
130
+ aux_reader = CachedPrefetchReader(
131
+ aux_reader,
132
+ prefetch_period=prefetch_period,
133
+ cache_size_mb=cache_size_mb
134
+ )
135
+
136
+ _aux = TimeGuardedWrapper(aux_reader, time_guard=time_provider)
119
137
  return _aux
120
138
  # fmt: on
121
139
 
@@ -777,6 +795,7 @@ def recognize_simulation_data_config(
777
795
  instruments: list[Instrument],
778
796
  open_close_time_indent_secs: int = 1,
779
797
  aux_data: DataReader | None = None,
798
+ prefetch_config: PrefetchConfig | None = None,
780
799
  ) -> SimulationDataConfig:
781
800
  """
782
801
  Recognizes and configures simulation data based on the provided declarations.
@@ -920,5 +939,6 @@ def recognize_simulation_data_config(
920
939
 
921
940
  # - just pass it to config, TODO: we need to think how to handle auxiliary data provider better
922
941
  _setup_defaults.aux_data_provider = aux_data # type: ignore
942
+ _setup_defaults.prefetch_config = prefetch_config
923
943
 
924
944
  return _setup_defaults
@@ -444,7 +444,7 @@ class CcxtDataProvider(IDataProvider):
444
444
  logger.info(f"<yellow>{self._exchange_id}</yellow> {name} listening has been stopped")
445
445
  break
446
446
  except (NetworkError, ExchangeError, ExchangeNotAvailable) as e:
447
- logger.error(f"<yellow>{self._exchange_id}</yellow> Error in {name} : {e}")
447
+ logger.error(f"<yellow>{self._exchange_id}</yellow> {e.__class__.__name__} :: Error in {name} : {e}")
448
448
  await asyncio.sleep(1)
449
449
  continue
450
450
  except Exception as e:
@@ -10,7 +10,7 @@ from qubx.core.basics import DataType, Instrument
10
10
  from qubx.data.readers import DataReader, DataTransformer
11
11
  from qubx.data.registry import reader
12
12
  from qubx.utils.misc import AsyncThreadLoop
13
- from qubx.utils.time import handle_start_stop
13
+ from qubx.utils.time import handle_start_stop, now_utc
14
14
 
15
15
  from .factory import get_ccxt_exchange
16
16
  from .utils import ccxt_find_instrument, instrument_to_ccxt_symbol
@@ -123,7 +123,7 @@ class CcxtDataReader(DataReader):
123
123
  if dtype != "ohlc":
124
124
  return None, None
125
125
 
126
- end_time = pd.Timestamp.now()
126
+ end_time = now_utc()
127
127
  start_time = end_time - self._max_history
128
128
  return start_time.to_datetime64(), end_time.to_datetime64()
129
129
 
@@ -153,14 +153,14 @@ class CcxtDataReader(DataReader):
153
153
  self, start: str | None, stop: str | None, timeframe: pd.Timedelta
154
154
  ) -> tuple[pd.Timestamp, pd.Timestamp]:
155
155
  if not stop:
156
- stop = pd.Timestamp.now().isoformat()
156
+ stop = now_utc().isoformat()
157
157
  _start, _stop = handle_start_stop(start, stop, convert=lambda x: pd.Timestamp(x))
158
158
  assert isinstance(_stop, pd.Timestamp)
159
159
  if not _start:
160
160
  _start = _stop - timeframe * self._max_bars
161
161
  assert isinstance(_start, pd.Timestamp)
162
162
 
163
- if _start < (_max_time := pd.Timestamp.now() - self._max_history):
163
+ if _start < (_max_time := now_utc() - self._max_history):
164
164
  _start = _max_time
165
165
 
166
166
  return _start, _stop
@@ -3,7 +3,6 @@ from typing import Any, Dict, List
3
3
 
4
4
  import numpy as np
5
5
  import pandas as pd
6
- from numba import njit
7
6
 
8
7
  import ccxt.pro as cxp
9
8
  from ccxt import BadSymbol
@@ -24,6 +23,7 @@ from qubx.utils.marketdata.ccxt import (
24
23
  ccxt_symbol_to_instrument,
25
24
  )
26
25
  from qubx.utils.orderbook import accumulate_orderbook_levels
26
+ from qubx.utils.time import to_utc_naive
27
27
 
28
28
  from .exceptions import (
29
29
  CcxtLiquidationParsingError,
@@ -244,7 +244,7 @@ def ccxt_convert_orderbook(
244
244
 
245
245
  def ccxt_convert_liquidation(liq: dict[str, Any]) -> Liquidation:
246
246
  try:
247
- _dt = pd.Timestamp(liq["datetime"]).replace(tzinfo=None).asm8
247
+ _dt = to_utc_naive(pd.Timestamp(liq["datetime"])).asm8
248
248
  return Liquidation(
249
249
  time=_dt,
250
250
  price=liq["price"],
@@ -265,7 +265,7 @@ def ccxt_convert_ticker(ticker: dict[str, Any]) -> Quote:
265
265
  Quote: The converted Quote object.
266
266
  """
267
267
  return Quote(
268
- time=pd.Timestamp(ticker["datetime"]).replace(tzinfo=None).asm8,
268
+ time=to_utc_naive(pd.Timestamp(ticker["datetime"])).asm8,
269
269
  bid=ticker["bid"],
270
270
  ask=ticker["ask"],
271
271
  bid_size=ticker["bidVolume"],