Qubx 0.6.53__tar.gz → 0.6.56__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 (177) hide show
  1. {qubx-0.6.53 → qubx-0.6.56}/PKG-INFO +1 -1
  2. {qubx-0.6.53 → qubx-0.6.56}/pyproject.toml +1 -1
  3. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/backtester/management.py +207 -76
  4. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/backtester/runner.py +1 -1
  5. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/basics.py +111 -5
  6. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/helpers.py +18 -7
  7. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/lookups.py +226 -304
  8. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/mixins/processing.py +76 -17
  9. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/data/composite.py +13 -1
  10. qubx-0.6.56/src/qubx/resources/crypto-fees.ini +98 -0
  11. qubx-0.6.56/src/qubx/resources/instruments/symbols-binance-spot.json +1 -0
  12. qubx-0.6.56/src/qubx/resources/instruments/symbols-binance.cm-future.json +1 -0
  13. qubx-0.6.56/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +1 -0
  14. qubx-0.6.56/src/qubx/resources/instruments/symbols-binance.um-future.json +1 -0
  15. qubx-0.6.56/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +1 -0
  16. qubx-0.6.56/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +1 -0
  17. qubx-0.6.56/src/qubx/resources/instruments/symbols-hyperliquid-spot.json +1 -0
  18. qubx-0.6.56/src/qubx/resources/instruments/symbols-hyperliquid.f-perpetual.json +1 -0
  19. qubx-0.6.56/src/qubx/resources/instruments/symbols-kraken-spot.json +1 -0
  20. qubx-0.6.56/src/qubx/resources/instruments/symbols-kraken.f-future.json +1 -0
  21. qubx-0.6.56/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +1 -0
  22. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/restorers/factory.py +3 -3
  23. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/marketdata/ccxt.py +51 -6
  24. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/marketdata/dukas.py +1 -1
  25. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/runner/runner.py +1 -1
  26. qubx-0.6.53/src/qubx/resources/instruments/symbols-binance.cm.json +0 -1
  27. qubx-0.6.53/src/qubx/resources/instruments/symbols-binance.json +0 -1
  28. qubx-0.6.53/src/qubx/resources/instruments/symbols-binance.um.json +0 -1
  29. qubx-0.6.53/src/qubx/resources/instruments/symbols-bitfinex.f.json +0 -1
  30. qubx-0.6.53/src/qubx/resources/instruments/symbols-bitfinex.json +0 -1
  31. qubx-0.6.53/src/qubx/resources/instruments/symbols-kraken.f.json +0 -1
  32. qubx-0.6.53/src/qubx/resources/instruments/symbols-kraken.json +0 -1
  33. {qubx-0.6.53 → qubx-0.6.56}/LICENSE +0 -0
  34. {qubx-0.6.53 → qubx-0.6.56}/README.md +0 -0
  35. {qubx-0.6.53 → qubx-0.6.56}/build.py +0 -0
  36. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/__init__.py +0 -0
  37. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/_nb_magic.py +0 -0
  38. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/backtester/__init__.py +0 -0
  39. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/backtester/account.py +0 -0
  40. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/backtester/broker.py +0 -0
  41. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/backtester/data.py +0 -0
  42. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/backtester/ome.py +0 -0
  43. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/backtester/optimization.py +0 -0
  44. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/backtester/simulated_data.py +0 -0
  45. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/backtester/simulated_exchange.py +0 -0
  46. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/backtester/simulator.py +0 -0
  47. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/backtester/utils.py +0 -0
  48. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/cli/__init__.py +0 -0
  49. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/cli/commands.py +0 -0
  50. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/cli/deploy.py +0 -0
  51. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/cli/misc.py +0 -0
  52. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/cli/release.py +0 -0
  53. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/cli/tui.py +0 -0
  54. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/__init__.py +0 -0
  55. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/account.py +0 -0
  56. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/broker.py +0 -0
  57. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/data.py +0 -0
  58. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  59. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
  60. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  61. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
  62. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  63. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  64. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  65. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/factory.py +0 -0
  66. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/reader.py +0 -0
  67. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/ccxt/utils.py +0 -0
  68. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/tardis/data.py +0 -0
  69. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/connectors/tardis/utils.py +0 -0
  70. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/__init__.py +0 -0
  71. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/account.py +0 -0
  72. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/context.py +0 -0
  73. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/deque.py +0 -0
  74. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/errors.py +0 -0
  75. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/exceptions.py +0 -0
  76. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/initializer.py +0 -0
  77. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/interfaces.py +0 -0
  78. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/loggers.py +0 -0
  79. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/metrics.py +0 -0
  80. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/mixins/__init__.py +0 -0
  81. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/mixins/market.py +0 -0
  82. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/mixins/subscription.py +0 -0
  83. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/mixins/trading.py +0 -0
  84. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/mixins/universe.py +0 -0
  85. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/series.pxd +0 -0
  86. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/series.pyi +0 -0
  87. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/series.pyx +0 -0
  88. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/utils.pyi +0 -0
  89. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/core/utils.pyx +0 -0
  90. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/data/__init__.py +0 -0
  91. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/data/helpers.py +0 -0
  92. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/data/hft.py +0 -0
  93. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/data/readers.py +0 -0
  94. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/data/registry.py +0 -0
  95. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/data/tardis.py +0 -0
  96. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/emitters/__init__.py +0 -0
  97. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/emitters/base.py +0 -0
  98. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/emitters/composite.py +0 -0
  99. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/emitters/csv.py +0 -0
  100. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/emitters/prometheus.py +0 -0
  101. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/emitters/questdb.py +0 -0
  102. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/exporters/__init__.py +0 -0
  103. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/exporters/composite.py +0 -0
  104. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/exporters/formatters/__init__.py +0 -0
  105. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/exporters/formatters/base.py +0 -0
  106. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/exporters/formatters/incremental.py +0 -0
  107. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/exporters/formatters/slack.py +0 -0
  108. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/exporters/redis_streams.py +0 -0
  109. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/exporters/slack.py +0 -0
  110. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/features/__init__.py +0 -0
  111. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/features/core.py +0 -0
  112. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/features/orderbook.py +0 -0
  113. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/features/price.py +0 -0
  114. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/features/trades.py +0 -0
  115. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/features/utils.py +0 -0
  116. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/gathering/simplest.py +0 -0
  117. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/health/__init__.py +0 -0
  118. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/health/base.py +0 -0
  119. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/loggers/__init__.py +0 -0
  120. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/loggers/csv.py +0 -0
  121. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/loggers/factory.py +0 -0
  122. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/loggers/inmemory.py +0 -0
  123. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/loggers/mongo.py +0 -0
  124. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/math/__init__.py +0 -0
  125. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/math/stats.py +0 -0
  126. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/notifications/__init__.py +0 -0
  127. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/notifications/composite.py +0 -0
  128. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/notifications/slack.py +0 -0
  129. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/notifications/throttler.py +0 -0
  130. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/pandaz/__init__.py +0 -0
  131. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/pandaz/ta.py +0 -0
  132. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/pandaz/utils.py +0 -0
  133. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/resources/_build.py +0 -0
  134. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/restarts/__init__.py +0 -0
  135. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/restarts/state_resolvers.py +0 -0
  136. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/restarts/time_finders.py +0 -0
  137. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/restorers/__init__.py +0 -0
  138. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/restorers/balance.py +0 -0
  139. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/restorers/interfaces.py +0 -0
  140. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/restorers/position.py +0 -0
  141. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/restorers/signal.py +0 -0
  142. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/restorers/state.py +0 -0
  143. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/restorers/utils.py +0 -0
  144. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/ta/__init__.py +0 -0
  145. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/ta/indicators.pxd +0 -0
  146. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/ta/indicators.pyi +0 -0
  147. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/ta/indicators.pyx +0 -0
  148. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/trackers/__init__.py +0 -0
  149. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/trackers/advanced.py +0 -0
  150. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/trackers/composite.py +0 -0
  151. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/trackers/rebalancers.py +0 -0
  152. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/trackers/riskctrl.py +0 -0
  153. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/trackers/sizers.py +0 -0
  154. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/__init__.py +0 -0
  155. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/_pyxreloader.py +0 -0
  156. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/charting/lookinglass.py +0 -0
  157. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  158. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/collections.py +0 -0
  159. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/marketdata/binance.py +0 -0
  160. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/misc.py +0 -0
  161. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/ntp.py +0 -0
  162. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/numbers_utils.py +0 -0
  163. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/orderbook.py +0 -0
  164. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/plotting/__init__.py +0 -0
  165. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/plotting/dashboard.py +0 -0
  166. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/plotting/data.py +0 -0
  167. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/plotting/interfaces.py +0 -0
  168. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  169. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  170. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/questdb.py +0 -0
  171. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/runner/__init__.py +0 -0
  172. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
  173. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/runner/accounts.py +0 -0
  174. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/runner/configs.py +0 -0
  175. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/runner/factory.py +0 -0
  176. {qubx-0.6.53 → qubx-0.6.56}/src/qubx/utils/time.py +0 -0
  177. {qubx-0.6.53 → qubx-0.6.56}/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.53
3
+ Version: 0.6.56
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.53"
7
+ version = "0.6.56"
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"
@@ -3,6 +3,7 @@ import zipfile
3
3
  from collections import defaultdict
4
4
  from pathlib import Path
5
5
 
6
+ import numpy as np
6
7
  import pandas as pd
7
8
  import yaml
8
9
 
@@ -230,6 +231,25 @@ class BacktestsResultsManager:
230
231
 
231
232
  yield info.get("idx", -1)
232
233
 
234
+ def list_variations(self, regex: str = "", detailed=True, sort_by: str | None = "sharpe", ascending=False):
235
+ """
236
+ List only variations of a backtest result.
237
+
238
+ Args:
239
+ - regex (str, optional): Regular expression pattern to filter results by strategy name or class. Defaults to "".
240
+ - sort_by (str, optional): The criterion to sort the results by. Defaults to "sharpe".
241
+ - ascending (bool, optional): Whether to sort the results in ascending order. Defaults to False.
242
+ - detailed (bool, optional): Whether to show each variation run. Defaults to True.
243
+ """
244
+ return self.list(
245
+ regex=regex,
246
+ sort_by=sort_by,
247
+ ascending=ascending,
248
+ show_variations=True,
249
+ show_simulations=False,
250
+ show_each_variation_run=detailed,
251
+ )
252
+
233
253
  def list(
234
254
  self,
235
255
  regex: str = "",
@@ -239,7 +259,9 @@ class BacktestsResultsManager:
239
259
  pretty_print=False,
240
260
  sort_by: str | None = "sharpe",
241
261
  ascending=False,
262
+ show_simulations=True,
242
263
  show_variations=True,
264
+ show_each_variation_run=True,
243
265
  ):
244
266
  """List backtesting results with optional filtering and formatting.
245
267
 
@@ -248,87 +270,93 @@ class BacktestsResultsManager:
248
270
  - with_metrics (bool, optional): Whether to include performance metrics in output. Defaults to True.
249
271
  - params (bool, optional): Whether to display strategy parameters. Defaults to False.
250
272
  - as_table (bool, optional): Return results as a pandas DataFrame instead of printing. Defaults to False.
273
+ - sort_by (str, optional): The criterion to sort the results by. Defaults to "sharpe".
274
+ - ascending (bool, optional): Whether to sort the results in ascending order. Defaults to False.
275
+ - show_simulations (bool, optional): Whether to show simulation results. Defaults to True.
276
+ - show_variations (bool, optional): Whether to show variation results. Defaults to True.
277
+ - show_each_variation_run (bool, optional): Whether to show each variation run. Defaults to True.
251
278
 
252
279
  Returns:
253
280
  - Optional[pd.DataFrame]: If as_table=True, returns a DataFrame containing the results sorted by creation time.
254
281
  - Otherwise prints formatted results to console.
255
282
  """
256
283
  _t_rep = []
257
- for n in sorted(self.results.keys()):
258
- info = self.results[n]
259
- s_cls = info.get("strategy_class", "").split(".")[-1]
284
+ if show_simulations:
285
+ for n in sorted(self.results.keys()):
286
+ info = self.results[n]
287
+ s_cls = info.get("strategy_class", "").split(".")[-1]
260
288
 
261
- if regex:
262
- if not re.match(regex, n, re.IGNORECASE):
263
- # if not re.match(regex, s_cls, re.IGNORECASE):
264
- continue
289
+ if regex:
290
+ if not re.match(regex, n, re.IGNORECASE):
291
+ # if not re.match(regex, s_cls, re.IGNORECASE):
292
+ continue
265
293
 
266
- name = info.get("name", "")
267
- smbs = ", ".join(info.get("symbols", list()))
268
- start = pd.Timestamp(info.get("start", "")).round("1s")
269
- stop = pd.Timestamp(info.get("stop", "")).round("1s")
270
- dscr = info.get("description", "")
271
- created = pd.Timestamp(info.get("creation_time", "")).round("1s")
272
- metrics = info.get("performance", {})
273
- author = info.get("author", "")
274
- _s = f"{yellow(str(info.get('idx')))} - {red(name)} ::: {magenta(created)} by {cyan(author)}"
275
-
276
- _one_line_dscr = ""
277
- if dscr:
278
- dscr = dscr.split("\n")
279
- for _d in dscr:
280
- _s += f"\n\t{magenta('# ' + _d)}"
281
- _one_line_dscr += "\u25cf " + _d + "\n"
282
-
283
- _s += f"\n\tstrategy: {green(s_cls)}"
284
- _s += f"\n\tinterval: {blue(start)} - {blue(stop)}"
285
- _s += f"\n\tcapital: {blue(info.get('capital', ''))} {info.get('base_currency', '')} ({info.get('commissions', '')})"
286
- _s += f"\n\tinstruments: {blue(smbs)}"
287
- if params:
288
- formats = ["{" + f":<{i}" + "}" for i in [50]]
289
- _p = pd.DataFrame.from_dict(info.get("parameters", {}), orient="index")
290
- for i in _p.to_string(
291
- max_colwidth=30,
292
- header=False,
293
- formatters=[(lambda x: cyan(fmt.format(str(x)))) for fmt in formats],
294
- justify="left",
295
- ).split("\n"):
296
- _s += f"\n\t | {yellow(i)}"
297
-
298
- if not as_table:
299
- print(_s)
294
+ name = info.get("name", "")
295
+ smbs = ", ".join(info.get("symbols", list()))
296
+ start = pd.Timestamp(info.get("start", "")).round("1s")
297
+ stop = pd.Timestamp(info.get("stop", "")).round("1s")
298
+ dscr = info.get("description", "")
299
+ created = pd.Timestamp(info.get("creation_time", "")).round("1s")
300
+ metrics = info.get("performance", {})
301
+ author = info.get("author", "")
302
+ _s = f"{yellow(str(info.get('idx')))} - {red(name)} ::: {magenta(created)} by {cyan(author)}"
303
+
304
+ _one_line_dscr = ""
305
+ if dscr:
306
+ dscr = dscr.split("\n")
307
+ for _d in dscr:
308
+ _s += f"\n\t{magenta('# ' + _d)}"
309
+ _one_line_dscr += "\u25cf " + _d + "\n"
310
+
311
+ _s += f"\n\tstrategy: {green(s_cls)}"
312
+ _s += f"\n\tinterval: {blue(start)} - {blue(stop)}"
313
+ _s += f"\n\tcapital: {blue(info.get('capital', ''))} {info.get('base_currency', '')} ({info.get('commissions', '')})"
314
+ _s += f"\n\tinstruments: {blue(smbs)}"
315
+ if params:
316
+ formats = ["{" + f":<{i}" + "}" for i in [50]]
317
+ _p = pd.DataFrame.from_dict(info.get("parameters", {}), orient="index")
318
+ for i in _p.to_string(
319
+ max_colwidth=30,
320
+ header=False,
321
+ formatters=[(lambda x: cyan(fmt.format(str(x)))) for fmt in formats],
322
+ justify="left",
323
+ ).split("\n"):
324
+ _s += f"\n\t | {yellow(i)}"
300
325
 
301
- if with_metrics:
302
- _m_repr = (
303
- pd.DataFrame.from_dict(metrics, orient="index")
304
- .T[["gain", "cagr", "sharpe", "qr", "max_dd_pct", "mdd_usd", "fees", "execs"]]
305
- .astype(float)
306
- )
307
- _m_repr = _m_repr.round(3).to_string(index=False)
308
- _h, _v = _m_repr.split("\n")
309
326
  if not as_table:
310
- print("\t " + red(_h))
311
- print("\t " + cyan(_v))
312
-
313
- if not as_table:
314
- print()
315
- else:
316
- metrics = {
317
- m: round(v, 3)
318
- for m, v in metrics.items()
319
- if m in ["gain", "cagr", "sharpe", "qr", "max_dd_pct", "mdd_usd", "fees", "execs"]
320
- }
321
- _t_rep.append(
322
- {"Index": info.get("idx", ""), "Strategy": name}
323
- | metrics
324
- | {
325
- "start": start,
326
- "stop": stop,
327
- "Created": created,
328
- "Author": author,
329
- "Description": _one_line_dscr,
330
- },
331
- )
327
+ print(_s)
328
+
329
+ if with_metrics:
330
+ _m_repr = (
331
+ pd.DataFrame.from_dict(metrics, orient="index")
332
+ .T[["gain", "cagr", "sharpe", "qr", "max_dd_pct", "mdd_usd", "fees", "execs"]]
333
+ .astype(float)
334
+ )
335
+ _m_repr = _m_repr.round(3).to_string(index=False)
336
+ _h, _v = _m_repr.split("\n")
337
+ if not as_table:
338
+ print("\t " + red(_h))
339
+ print("\t " + cyan(_v))
340
+
341
+ if not as_table:
342
+ print()
343
+ else:
344
+ metrics = {
345
+ m: round(v, 3)
346
+ for m, v in metrics.items()
347
+ if m in ["gain", "cagr", "sharpe", "qr", "max_dd_pct", "mdd_usd", "fees", "execs"]
348
+ }
349
+ _t_rep.append(
350
+ {"Index": info.get("idx", ""), "Strategy": name}
351
+ | metrics
352
+ | {
353
+ "start": start,
354
+ "stop": stop,
355
+ "Created": created,
356
+ "Author": author,
357
+ "Description": _one_line_dscr,
358
+ },
359
+ )
332
360
 
333
361
  # - variations (only if not as_table for the time being)
334
362
  if not as_table and show_variations:
@@ -358,11 +386,12 @@ class BacktestsResultsManager:
358
386
  _m_repr = _m_repr.to_string(index=True)
359
387
 
360
388
  print(_s)
361
- for _i, _l in enumerate(_m_repr.split("\n")):
362
- if _i == 0:
363
- print("\t " + red(_l))
364
- else:
365
- print("\t " + blue(_l))
389
+ if show_each_variation_run:
390
+ for _i, _l in enumerate(_m_repr.split("\n")):
391
+ if _i == 0:
392
+ print("\t " + red(_l))
393
+ else:
394
+ print("\t " + blue(_l))
366
395
 
367
396
  if as_table:
368
397
  _df = pd.DataFrame.from_records(_t_rep, index="Index")
@@ -376,3 +405,105 @@ class BacktestsResultsManager:
376
405
  .replace("<td>", '<td align="left" valign="top">')
377
406
  )
378
407
  return _df
408
+
409
+ def variation_plot(self, variation_idx: int, criterion: str = "sharpe", ascending: bool = False, n=3, h=600):
410
+ """
411
+ Plot a variation of a backtest result.
412
+
413
+ Args:
414
+ - variation_idx (int): The index of the variation to plot.
415
+ - criterion (str): The criterion to plot (e.g. "sharpe", "mdd_usd", "max_dd_pct", etc.).
416
+ - ascending (bool): Whether to sort the results in ascending order.
417
+ - n (int): The number of decimal places to display.
418
+ - h (int): The height of the plot.
419
+
420
+ Returns:
421
+ plotly.graph_objects.Figure: The plot of the variation.
422
+ """
423
+ import plotly.express as px
424
+ from itertools import cycle
425
+ from qubx.utils.misc import string_shortener
426
+
427
+ _vars = self.variations.get(variation_idx)
428
+ if not _vars:
429
+ raise ValueError(f"No variations found for index {variation_idx} !")
430
+
431
+ variations = _vars.get("variations", [])
432
+ name = _vars.get("name", "") or ""
433
+
434
+ _r, _p = {}, {}
435
+ for i, v in enumerate(variations):
436
+ _p[i] = v["parameters"]
437
+ _pp = pd.DataFrame.from_records(_p).T
438
+ # - changed parameters
439
+ _cp = []
440
+ for c in _pp.columns:
441
+ if len(_pp[c].astype(str).unique()) > 1:
442
+ _cp.append(c)
443
+
444
+ # - if nothing was actually changed in parameters, raise an error
445
+ if not _cp:
446
+ raise ValueError(f"No variable parameters found for simulation {name} !")
447
+
448
+ _ms = max([len(string_shortener(x)) for x in _cp]) + 3
449
+ _h = "".join([string_shortener(x).center(_ms) for x in _cp])
450
+
451
+ _sel = lambda ds, _cp: "".join(
452
+ [
453
+ f"<span style='color:{c}'> {str(ds[k]).center(_ms)}</span>"
454
+ for k, c in zip(_cp, cycle(px.colors.qualitative.Plotly))
455
+ if k in k in ds
456
+ ]
457
+ )
458
+ for i, v in enumerate(variations):
459
+ _r[i] = {"name": v["name"], **v["performance"], "parameters": _sel(v["parameters"], _cp)}
460
+
461
+ t1 = pd.DataFrame.from_records(_r).T
462
+ if criterion not in t1.columns:
463
+ raise ValueError(f"Criterion {criterion} not found in results: possible values are {t1.columns}")
464
+ t2 = t1.sort_values(criterion, ascending=ascending)
465
+
466
+ data = pd.Series([np.nan, *t2[criterion].to_list()], index=[_h, *t2["parameters"].to_list()])
467
+
468
+ figure = (
469
+ px.bar(data, orientation="h")
470
+ .update_layout(
471
+ title=dict(
472
+ text=f"{name} | <span style='color:orange'>{criterion.capitalize()}</span>",
473
+ ),
474
+ xaxis=dict(tickfont=dict(family="monospace", size=10, color="#ff4000")),
475
+ yaxis=dict(
476
+ tickfont=dict(family="monospace", size=10, color="#40a000"),
477
+ dtick=1,
478
+ ),
479
+ )
480
+ .update_layout(
481
+ height=h,
482
+ hovermode="x unified",
483
+ showlegend=False,
484
+ hoverdistance=1,
485
+ yaxis={"hoverformat": f".{n}f"},
486
+ dragmode="zoom",
487
+ newshape=dict(line_color="red", line_width=1.0),
488
+ modebar_add=["drawline", "drawopenpath", "drawrect", "eraseshape"],
489
+ hoverlabel=dict(align="auto", bgcolor="rgba(10, 10, 10, 0.5)"),
490
+ )
491
+ .update_xaxes(
492
+ showspikes=True,
493
+ spikemode="across",
494
+ spikesnap="cursor",
495
+ spikecolor="#306020",
496
+ spikethickness=1,
497
+ spikedash="dot",
498
+ title=criterion,
499
+ )
500
+ .update_yaxes(
501
+ spikesnap="cursor",
502
+ spikecolor="#306020",
503
+ tickformat=f".{n}f",
504
+ spikethickness=1,
505
+ title="Parameters",
506
+ autorange="reversed",
507
+ )
508
+ )
509
+ return figure
@@ -439,7 +439,7 @@ class SimulationRunner:
439
439
  if isinstance(commissions, (str, type(None))):
440
440
  commissions = {e: commissions for e in exchanges}
441
441
  for exchange in exchanges:
442
- _exchange_to_tcc[exchange] = lookup.fees.find(exchange.lower(), commissions.get(exchange))
442
+ _exchange_to_tcc[exchange] = lookup.find_fees(exchange.lower(), commissions.get(exchange))
443
443
  return _exchange_to_tcc
444
444
 
445
445
  def _construct_account_processor(
@@ -1,3 +1,4 @@
1
+ import re
1
2
  from dataclasses import dataclass, field
2
3
  from datetime import datetime
3
4
  from enum import StrEnum
@@ -176,6 +177,14 @@ class MarketType(StrEnum):
176
177
 
177
178
  @dataclass
178
179
  class Instrument:
180
+ """
181
+ Instrument class.
182
+
183
+ - 2025-06-11: Important change for FUTURE type: now instrument's symbol contains delivery date in format YYYYMMDD.
184
+ So now for let's say september's BTCUSDT future, symbol would be BTCUSD.20250914
185
+ and full id is `BINANCE.UM:FUTURE:BTCUSD.20250914`
186
+ """
187
+
179
188
  symbol: str
180
189
  asset_type: AssetType
181
190
  market_type: MarketType
@@ -192,8 +201,10 @@ class Instrument:
192
201
  maint_margin: float = 0.0 # maintenance margin
193
202
  liquidation_fee: float = 0.0 # liquidation fee
194
203
  contract_size: float = 1.0 # contract size
195
- onboard_date: datetime | None = None
196
- delivery_date: datetime | None = None
204
+ onboard_date: datetime | None = None # date when instrument was listed on the exchange
205
+ delivery_date: datetime | None = None # date when instrument is delivered
206
+ delist_date: datetime | None = None # date when instrument is delisted
207
+ inverse: bool = False # if true, then the future is inverse
197
208
 
198
209
  @property
199
210
  def price_precision(self):
@@ -437,9 +448,6 @@ class AssetBalance:
437
448
  return self
438
449
 
439
450
 
440
- MARKET_TYPE = Literal["SPOT", "MARGIN", "SWAP", "FUTURES", "OPTION"]
441
-
442
-
443
451
  class Position:
444
452
  instrument: Instrument # instrument for this position
445
453
  quantity: float = 0.0 # quantity positive for long and negative for short
@@ -878,3 +886,101 @@ class RestoredState:
878
886
  balances: dict[str, AssetBalance]
879
887
  instrument_to_target_positions: dict[Instrument, list[TargetPosition]]
880
888
  positions: dict[Instrument, Position]
889
+
890
+
891
+ class InstrumentsLookup:
892
+ def get_lookup(self) -> dict[str, Instrument]: ...
893
+
894
+ def find(
895
+ self,
896
+ exchange: str,
897
+ base: str,
898
+ quote: str,
899
+ settle: str | None = None,
900
+ market_type: MarketType | None = None,
901
+ ) -> Instrument | None:
902
+ for i in self.get_lookup().values():
903
+ if (
904
+ i.exchange == exchange
905
+ and ((i.base == base and i.quote == quote) or (i.base == quote and i.quote == base))
906
+ and (market_type is None or i.market_type == market_type)
907
+ ):
908
+ if settle is not None and i.settle is not None:
909
+ if i.settle == settle:
910
+ return i
911
+ else:
912
+ return i
913
+ return None
914
+
915
+ def find_symbol(self, exchange: str, symbol: str, market_type: MarketType | None = None) -> Instrument | None:
916
+ for i in self.get_lookup().values():
917
+ if (
918
+ (i.exchange == exchange)
919
+ and (i.symbol == symbol)
920
+ and (market_type is None or i.market_type == market_type)
921
+ ):
922
+ return i
923
+
924
+ return None
925
+
926
+ def find_instruments(
927
+ self,
928
+ exchange: str,
929
+ quote: str | None = None,
930
+ market_type: MarketType | None = None,
931
+ as_of: str | pd.Timestamp | None = None,
932
+ ) -> list[Instrument]:
933
+ """
934
+ Find instruments by exchange, quote, market type and as of date.
935
+ If as_of is not None, then only instruments that are not delisted after as_of date will be returned.
936
+ - exchange: str - exchange name
937
+ - quote: str | None - quote currency
938
+ - market_type: MarketType | None - market type
939
+ - as_of is a string in format YYYY-MM-DD or pd.Timestamp or None
940
+ """
941
+ _limit_time = pd.Timestamp(as_of) if as_of else None
942
+ return [
943
+ i
944
+ for i in self.get_lookup().values()
945
+ if i.exchange == exchange
946
+ and (quote is None or i.quote == quote)
947
+ and (market_type is None or i.market_type == market_type)
948
+ and (
949
+ _limit_time is None
950
+ or (i.delist_date is None)
951
+ or (pd.Timestamp(i.delist_date).tz_localize(None) >= _limit_time)
952
+ )
953
+ ]
954
+
955
+ def find_aux_instrument_for(
956
+ self, instrument: Instrument, base_currency: str, market_type: MarketType | None = None
957
+ ) -> Instrument | None:
958
+ """
959
+ Tries to find aux instrument (for conversions to funded currency)
960
+ for example:
961
+ ETHBTC -> BTCUSDT for base_currency USDT
962
+ EURGBP -> GBPUSD for base_currency USD
963
+ ...
964
+ """
965
+ if market_type is None:
966
+ market_type = instrument.market_type
967
+ base_currency = base_currency.upper()
968
+ if instrument.quote != base_currency:
969
+ return self.find(instrument.exchange, instrument.quote, base_currency, market_type=market_type)
970
+ return None
971
+
972
+ def __getitem__(self, spath: str) -> list[Instrument]:
973
+ """
974
+ Helper method for finding instruments by pattern.
975
+ It's convenient to use in research mode.
976
+ """
977
+ res = []
978
+ c = re.compile(spath)
979
+ for k, v in self.get_lookup().items():
980
+ if re.match(c, k):
981
+ res.append(v)
982
+ return res
983
+
984
+
985
+ class FeesLookup:
986
+ def find_fees(self, exchange: str, spec: str | None) -> TransactionCostsCalculator: ...
@@ -6,15 +6,15 @@ import time
6
6
  from collections import defaultdict, deque
7
7
  from inspect import isbuiltin, isclass, isfunction, ismethod, ismethoddescriptor
8
8
  from threading import Thread
9
- from typing import Any, Callable, Dict, List
9
+ from typing import Any, Callable
10
10
 
11
11
  import numpy as np
12
12
  import pandas as pd
13
13
  from croniter import croniter
14
14
 
15
15
  from qubx import logger
16
- from qubx.core.basics import SW, CtrlChannel, DataType, Instrument, Timestamped
17
- from qubx.core.series import OHLCV, Bar, OrderBook, Quote, Trade
16
+ from qubx.core.basics import SW, CtrlChannel, DataType, Instrument, Timestamped, dt_64
17
+ from qubx.core.series import OHLCV, Bar, OrderBook, Quote, Trade, time_as_nsec
18
18
  from qubx.utils.time import convert_seconds_to_str, convert_tf_str_td64, interval_to_cron
19
19
 
20
20
 
@@ -26,9 +26,9 @@ class CachedMarketDataHolder:
26
26
  default_timeframe: np.timedelta64
27
27
  _last_bar: dict[Instrument, Bar | None]
28
28
  _ohlcvs: dict[Instrument, dict[np.timedelta64, OHLCV]]
29
- _updates: dict[Instrument, Any]
29
+ _updates: dict[Instrument, Bar | Quote | Trade]
30
30
 
31
- _instr_to_sub_to_buffer: Dict[Instrument, Dict[str, deque]]
31
+ _instr_to_sub_to_buffer: dict[Instrument, dict[str, deque]]
32
32
 
33
33
  def __init__(self, default_timeframe: str | None = None, max_buffer_size: int = 10_000) -> None:
34
34
  self._ohlcvs = dict()
@@ -93,7 +93,7 @@ class CachedMarketDataHolder:
93
93
 
94
94
  return self._ohlcvs[instrument][tf]
95
95
 
96
- def get_data(self, instrument: Instrument, event_type: str) -> List[Any]:
96
+ def get_data(self, instrument: Instrument, event_type: str) -> list[Any]:
97
97
  return list(self._instr_to_sub_to_buffer[instrument][event_type])
98
98
 
99
99
  def update(
@@ -126,7 +126,7 @@ class CachedMarketDataHolder:
126
126
  pass
127
127
 
128
128
  @SW.watch("CachedMarketDataHolder")
129
- def update_by_bars(self, instrument: Instrument, timeframe: str | np.timedelta64, bars: List[Bar]) -> OHLCV:
129
+ def update_by_bars(self, instrument: Instrument, timeframe: str | np.timedelta64, bars: list[Bar]) -> OHLCV:
130
130
  """
131
131
  Update or create OHLCV series with the provided historical bars.
132
132
 
@@ -203,6 +203,17 @@ class CachedMarketDataHolder:
203
203
  continue
204
204
  ser.update(trade.time, trade.price, total_vol, bought_vol)
205
205
 
206
+ def finalize_all_ohlc(self, time: dt_64):
207
+ """
208
+ Finalize all OHLCV series at the given time.
209
+ FIXME: (2025-06-17) This is part of urgent live fix and must be removed in future !!!.
210
+ """
211
+ for instrument in self._ohlcvs.keys():
212
+ # - use most recent update
213
+ if (_u := self._updates.get(instrument)) is not None:
214
+ _px = extract_price(_u)
215
+ self.update_by_bar(instrument, Bar(time_as_nsec(time), _px, _px, _px, _px, 0, 0))
216
+
206
217
 
207
218
  SPEC_REGEX = re.compile(
208
219
  r"((?P<type>[A-Za-z]+)(\.?(?P<timeframe>[0-9A-Za-z]+))?\ *:)?"