Qubx 0.6.62__tar.gz → 0.6.64__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.
Files changed (172) hide show
  1. {qubx-0.6.62 → qubx-0.6.64}/PKG-INFO +1 -1
  2. {qubx-0.6.62 → qubx-0.6.64}/pyproject.toml +1 -1
  3. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/backtester/data.py +5 -1
  4. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/backtester/ome.py +5 -3
  5. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/backtester/simulated_data.py +43 -1
  6. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/backtester/simulator.py +25 -15
  7. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/backtester/utils.py +68 -25
  8. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/account.py +81 -9
  9. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/basics.py +97 -3
  10. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/context.py +8 -3
  11. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/helpers.py +11 -4
  12. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/interfaces.py +36 -2
  13. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/loggers.py +19 -16
  14. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/lookups.py +7 -7
  15. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/metrics.py +42 -4
  16. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/mixins/market.py +22 -12
  17. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/mixins/processing.py +65 -8
  18. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/series.pyi +4 -3
  19. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/series.pyx +34 -12
  20. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/utils.pyx +3 -0
  21. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/data/helpers.py +75 -39
  22. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/data/readers.py +224 -15
  23. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/data/registry.py +1 -1
  24. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/emitters/__init__.py +2 -0
  25. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/emitters/base.py +23 -2
  26. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/emitters/composite.py +17 -2
  27. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/emitters/csv.py +43 -1
  28. qubx-0.6.64/src/qubx/emitters/inmemory.py +244 -0
  29. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/emitters/prometheus.py +57 -2
  30. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/emitters/questdb.py +131 -2
  31. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/features/core.py +11 -8
  32. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/features/orderbook.py +2 -1
  33. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/features/trades.py +1 -1
  34. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/gathering/simplest.py +7 -1
  35. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/loggers/inmemory.py +28 -13
  36. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/pandaz/ta.py +11 -20
  37. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/pandaz/utils.py +11 -0
  38. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/trackers/riskctrl.py +3 -4
  39. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/time.py +3 -1
  40. {qubx-0.6.62 → qubx-0.6.64}/LICENSE +0 -0
  41. {qubx-0.6.62 → qubx-0.6.64}/README.md +0 -0
  42. {qubx-0.6.62 → qubx-0.6.64}/build.py +0 -0
  43. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/__init__.py +0 -0
  44. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/_nb_magic.py +0 -0
  45. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/backtester/__init__.py +0 -0
  46. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/backtester/account.py +0 -0
  47. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/backtester/broker.py +0 -0
  48. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/backtester/management.py +0 -0
  49. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/backtester/optimization.py +0 -0
  50. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/backtester/runner.py +0 -0
  51. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/backtester/simulated_exchange.py +0 -0
  52. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/cli/__init__.py +0 -0
  53. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/cli/commands.py +0 -0
  54. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/cli/deploy.py +0 -0
  55. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/cli/misc.py +0 -0
  56. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/cli/release.py +0 -0
  57. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/cli/tui.py +0 -0
  58. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/__init__.py +0 -0
  59. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/account.py +0 -0
  60. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/broker.py +0 -0
  61. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/data.py +0 -0
  62. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  63. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
  64. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  65. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
  66. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  67. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  68. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  69. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/factory.py +0 -0
  70. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/reader.py +0 -0
  71. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/ccxt/utils.py +0 -0
  72. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/tardis/data.py +0 -0
  73. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/connectors/tardis/utils.py +0 -0
  74. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/__init__.py +0 -0
  75. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/deque.py +0 -0
  76. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/errors.py +0 -0
  77. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/exceptions.py +0 -0
  78. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/initializer.py +0 -0
  79. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/mixins/__init__.py +0 -0
  80. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/mixins/subscription.py +0 -0
  81. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/mixins/trading.py +0 -0
  82. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/mixins/universe.py +0 -0
  83. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/series.pxd +0 -0
  84. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/core/utils.pyi +0 -0
  85. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/data/__init__.py +0 -0
  86. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/data/composite.py +0 -0
  87. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/data/hft.py +0 -0
  88. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/data/tardis.py +0 -0
  89. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/emitters/indicator.py +0 -0
  90. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/exporters/__init__.py +0 -0
  91. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/exporters/composite.py +0 -0
  92. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/exporters/formatters/__init__.py +0 -0
  93. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/exporters/formatters/base.py +0 -0
  94. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/exporters/formatters/incremental.py +0 -0
  95. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/exporters/formatters/slack.py +0 -0
  96. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/exporters/redis_streams.py +0 -0
  97. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/exporters/slack.py +0 -0
  98. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/features/__init__.py +0 -0
  99. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/features/price.py +0 -0
  100. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/features/utils.py +0 -0
  101. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/health/__init__.py +0 -0
  102. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/health/base.py +0 -0
  103. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/loggers/__init__.py +0 -0
  104. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/loggers/csv.py +0 -0
  105. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/loggers/factory.py +0 -0
  106. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/loggers/mongo.py +0 -0
  107. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/math/__init__.py +0 -0
  108. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/math/stats.py +0 -0
  109. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/notifications/__init__.py +0 -0
  110. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/notifications/composite.py +0 -0
  111. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/notifications/slack.py +0 -0
  112. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/notifications/throttler.py +0 -0
  113. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/pandaz/__init__.py +0 -0
  114. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/resources/_build.py +0 -0
  115. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/resources/crypto-fees.ini +0 -0
  116. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  117. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  118. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  119. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  120. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  121. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  122. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/resources/instruments/symbols-hyperliquid-spot.json +0 -0
  123. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/resources/instruments/symbols-hyperliquid.f-perpetual.json +0 -0
  124. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  125. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  126. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  127. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/restarts/__init__.py +0 -0
  128. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/restarts/state_resolvers.py +0 -0
  129. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/restarts/time_finders.py +0 -0
  130. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/restorers/__init__.py +0 -0
  131. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/restorers/balance.py +0 -0
  132. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/restorers/factory.py +0 -0
  133. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/restorers/interfaces.py +0 -0
  134. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/restorers/position.py +0 -0
  135. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/restorers/signal.py +0 -0
  136. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/restorers/state.py +0 -0
  137. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/restorers/utils.py +0 -0
  138. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/ta/__init__.py +0 -0
  139. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/ta/indicators.pxd +0 -0
  140. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/ta/indicators.pyi +0 -0
  141. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/ta/indicators.pyx +0 -0
  142. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/trackers/__init__.py +0 -0
  143. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/trackers/advanced.py +0 -0
  144. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/trackers/composite.py +0 -0
  145. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/trackers/rebalancers.py +0 -0
  146. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/trackers/sizers.py +0 -0
  147. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/__init__.py +0 -0
  148. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/_pyxreloader.py +0 -0
  149. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/charting/lookinglass.py +0 -0
  150. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  151. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/collections.py +0 -0
  152. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/marketdata/binance.py +0 -0
  153. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/marketdata/ccxt.py +0 -0
  154. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/marketdata/dukas.py +0 -0
  155. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/misc.py +0 -0
  156. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/ntp.py +0 -0
  157. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/numbers_utils.py +0 -0
  158. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/orderbook.py +0 -0
  159. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/plotting/__init__.py +0 -0
  160. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/plotting/dashboard.py +0 -0
  161. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/plotting/data.py +0 -0
  162. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/plotting/interfaces.py +0 -0
  163. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  164. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  165. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/questdb.py +0 -0
  166. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/runner/__init__.py +0 -0
  167. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
  168. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/runner/accounts.py +0 -0
  169. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/runner/configs.py +0 -0
  170. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/runner/factory.py +0 -0
  171. {qubx-0.6.62 → qubx-0.6.64}/src/qubx/utils/runner/runner.py +0 -0
  172. {qubx-0.6.62 → qubx-0.6.64}/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.62
3
+ Version: 0.6.64
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.62"
7
+ version = "0.6.64"
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"
@@ -68,6 +68,10 @@ class SimulatedDataProvider(IDataProvider):
68
68
 
69
69
  # - provide historical data and last quote for subscribed instruments
70
70
  for i in _new_instr:
71
+ # Check if the instrument was actually subscribed (not filtered out)
72
+ if not self.has_subscription(i, subscription_type):
73
+ continue
74
+
71
75
  h_data = self._data_source.peek_historical_data(i, subscription_type)
72
76
  if h_data:
73
77
  # _s_type = DataType.from_str(subscription_type)[0]
@@ -119,7 +123,7 @@ class SimulatedDataProvider(IDataProvider):
119
123
  end = start - nbarsback * (_timeframe := pd.Timedelta(timeframe))
120
124
  _spec = f"{instrument.exchange}:{instrument.symbol}"
121
125
  return self._convert_records_to_bars(
122
- _reader.read(data_id=_spec, start=start, stop=end, transform=AsDict()), # type: ignore
126
+ _reader.read(data_id=_spec, start=start, stop=end, timeframe=timeframe, transform=AsDict()), # type: ignore
123
127
  time_as_nsec(self.time_provider.time()),
124
128
  _timeframe.asm8.item(),
125
129
  )
@@ -6,17 +6,17 @@ from sortedcontainers import SortedDict
6
6
 
7
7
  from qubx import logger
8
8
  from qubx.core.basics import (
9
+ OPTION_AVOID_STOP_ORDER_PRICE_VALIDATION,
9
10
  OPTION_FILL_AT_SIGNAL_PRICE,
10
11
  OPTION_SIGNAL_PRICE,
11
12
  OPTION_SKIP_PRICE_CROSS_CONTROL,
12
- OPTION_AVOID_STOP_ORDER_PRICE_VALIDATION,
13
13
  Deal,
14
14
  Instrument,
15
15
  ITimeProvider,
16
16
  Order,
17
17
  OrderSide,
18
- OrderType,
19
18
  OrderStatus,
19
+ OrderType,
20
20
  TransactionCostsCalculator,
21
21
  dt_64,
22
22
  )
@@ -208,7 +208,9 @@ class OrdersManagementEngine:
208
208
  **options,
209
209
  ) -> SimulatedExecutionReport:
210
210
  if self.bbo is None:
211
- raise ExchangeError(f"Simulator is not ready for order management - no quote for {self.instrument.symbol}")
211
+ raise SimulationError(
212
+ f"Simulator is not ready for order management - no quote for {self.instrument.symbol}"
213
+ )
212
214
 
213
215
  # - validate order parameters
214
216
  self._validate_order(order_side, order_type, amount, price, time_in_force, options)
@@ -3,11 +3,12 @@ from typing import Any, Iterator
3
3
  import pandas as pd
4
4
 
5
5
  from qubx import logger
6
- from qubx.core.basics import DataType, Instrument, Timestamped
6
+ from qubx.core.basics import DataType, Instrument, MarketType, Timestamped
7
7
  from qubx.core.exceptions import SimulationError
8
8
  from qubx.data.composite import IteratedDataStreamsSlicer
9
9
  from qubx.data.readers import (
10
10
  AsDict,
11
+ AsFundingPayments,
11
12
  AsOrderBook,
12
13
  AsQuotes,
13
14
  AsTrades,
@@ -87,6 +88,11 @@ class DataFetcher:
87
88
  self._producing_data_type = "orderbook"
88
89
  self._transformer = AsOrderBook()
89
90
 
91
+ case DataType.FUNDING_PAYMENT:
92
+ self._requested_data_type = "funding_payment"
93
+ self._producing_data_type = "funding_payment"
94
+ self._transformer = AsFundingPayments()
95
+
90
96
  case _:
91
97
  self._requested_data_type = subtype
92
98
  self._producing_data_type = subtype
@@ -250,9 +256,45 @@ class IterableSimulationData(Iterator):
250
256
  _access_key = f"{_subtype}"
251
257
  return _access_key, _subtype, _params
252
258
 
259
+ def _filter_instruments_for_subscription(self, data_type: str, instruments: list[Instrument]) -> list[Instrument]:
260
+ """
261
+ Filter instruments based on subscription type requirements.
262
+
263
+ For funding payment subscriptions, only SWAP instruments are supported since
264
+ funding payments are specific to perpetual swap contracts.
265
+
266
+ Args:
267
+ data_type: The data type being subscribed to
268
+ instruments: List of instruments to filter
269
+
270
+ Returns:
271
+ Filtered list of instruments appropriate for the subscription type
272
+ """
273
+ # Only funding payments require special filtering
274
+ if data_type == DataType.FUNDING_PAYMENT:
275
+ original_count = len(instruments)
276
+ filtered_instruments = [i for i in instruments if i.market_type == MarketType.SWAP]
277
+ filtered_count = len(filtered_instruments)
278
+
279
+ # Log if instruments were filtered out (debug info)
280
+ if filtered_count < original_count:
281
+ logger.debug(f"Filtered {original_count - filtered_count} non-SWAP instruments from funding payment subscription")
282
+
283
+ return filtered_instruments
284
+
285
+ # For all other subscription types, return instruments unchanged
286
+ return instruments
287
+
253
288
  def add_instruments_for_subscription(self, subscription: str, instruments: list[Instrument] | Instrument):
254
289
  instruments = instruments if isinstance(instruments, list) else [instruments]
255
290
  _subt_key, _data_type, _params = self._parse_subscription_spec(subscription)
291
+
292
+ # Filter instruments based on subscription type requirements
293
+ instruments = self._filter_instruments_for_subscription(_data_type, instruments)
294
+
295
+ # If no instruments remain after filtering, skip subscription
296
+ if not instruments:
297
+ return
256
298
 
257
299
  fetcher = self._subtyped_fetchers.get(_subt_key)
258
300
  if not fetcher:
@@ -4,12 +4,12 @@ import pandas as pd
4
4
  from joblib import delayed
5
5
 
6
6
  from qubx import QubxLogConfig, logger
7
+ from qubx.core.basics import Instrument
7
8
  from qubx.core.exceptions import SimulationError
8
9
  from qubx.core.metrics import TradingSessionResult
9
10
  from qubx.data.readers import DataReader
11
+ from qubx.emitters.inmemory import InMemoryMetricEmitter
10
12
  from qubx.utils.misc import ProgressParallel, Stopwatch, get_current_user
11
- from qubx.utils.runner.configs import EmissionConfig
12
- from qubx.utils.runner.factory import create_metric_emitters
13
13
  from qubx.utils.time import handle_start_stop
14
14
 
15
15
  from .runner import SimulationRunner
@@ -31,11 +31,11 @@ def simulate(
31
31
  strategies: StrategiesDecls_t,
32
32
  data: DataDecls_t,
33
33
  capital: float | dict[str, float],
34
- instruments: list[SymbolOrInstrument_t] | dict[ExchangeName_t, list[SymbolOrInstrument_t]],
34
+ instruments: list[str] | list[Instrument] | dict[ExchangeName_t, list[SymbolOrInstrument_t]],
35
35
  commissions: str | dict[str, str | None] | None,
36
36
  start: str | pd.Timestamp,
37
37
  stop: str | pd.Timestamp | None = None,
38
- exchange: ExchangeName_t | None = None,
38
+ exchange: ExchangeName_t | list[ExchangeName_t] | None = None,
39
39
  base_currency: str = "USDT",
40
40
  n_jobs: int = 1,
41
41
  silent: bool = False,
@@ -47,7 +47,8 @@ def simulate(
47
47
  show_latency_report: bool = False,
48
48
  portfolio_log_freq: str = "5Min",
49
49
  parallel_backend: Literal["loky", "multiprocessing"] = "multiprocessing",
50
- emission: EmissionConfig | None = None,
50
+ enable_inmemory_emitter: bool = False,
51
+ emitter_stats_interval: str = "1h",
51
52
  run_separate_instruments: bool = False,
52
53
  ) -> list[TradingSessionResult]:
53
54
  """
@@ -73,7 +74,8 @@ def simulate(
73
74
  - show_latency_report: If True, shows simulator's latency report.
74
75
  - portfolio_log_freq (str): Frequency for portfolio logging, default is "5Min".
75
76
  - parallel_backend (Literal["loky", "multiprocessing"]): Backend for parallel processing, default is "multiprocessing".
76
- - emission (EmissionConfig | None): Configuration for metric emitters, default is None.
77
+ - enable_inmemory_emitter (bool): If True, attaches an in-memory metric emitter and returns its dataframe in TradingSessionResult.emitter_data.
78
+ - emitter_stats_interval (str): Interval for emitting stats in the in-memory emitter (default: "1h").
77
79
  - run_separate_instruments (bool): If True, creates separate simulation setups for each instrument, default is False.
78
80
 
79
81
  Returns:
@@ -143,7 +145,8 @@ def simulate(
143
145
  show_latency_report=show_latency_report,
144
146
  portfolio_log_freq=portfolio_log_freq,
145
147
  parallel_backend=parallel_backend,
146
- emission=emission,
148
+ enable_inmemory_emitter=enable_inmemory_emitter,
149
+ emitter_stats_interval=emitter_stats_interval,
147
150
  )
148
151
 
149
152
 
@@ -157,7 +160,8 @@ def _run_setups(
157
160
  show_latency_report: bool = False,
158
161
  portfolio_log_freq: str = "5Min",
159
162
  parallel_backend: Literal["loky", "multiprocessing"] = "multiprocessing",
160
- emission: EmissionConfig | None = None,
163
+ enable_inmemory_emitter: bool = False,
164
+ emitter_stats_interval: str = "1h",
161
165
  ) -> list[TradingSessionResult]:
162
166
  # loggers don't work well with joblib and multiprocessing in general because they contain
163
167
  # open file handlers that cannot be pickled. I found a solution which requires the usage of enqueue=True
@@ -179,7 +183,8 @@ def _run_setups(
179
183
  silent,
180
184
  show_latency_report,
181
185
  portfolio_log_freq,
182
- emission,
186
+ enable_inmemory_emitter,
187
+ emitter_stats_interval,
183
188
  )
184
189
  for id, setup in enumerate(strategies_setups)
185
190
  ]
@@ -197,7 +202,8 @@ def _run_setups(
197
202
  silent,
198
203
  show_latency_report,
199
204
  portfolio_log_freq,
200
- emission,
205
+ enable_inmemory_emitter,
206
+ emitter_stats_interval,
201
207
  )
202
208
  for id, setup in enumerate(strategies_setups)
203
209
  )
@@ -223,14 +229,14 @@ def _run_setup(
223
229
  silent: bool,
224
230
  show_latency_report: bool,
225
231
  portfolio_log_freq: str,
226
- emission: EmissionConfig | None = None,
232
+ enable_inmemory_emitter: bool = False,
233
+ emitter_stats_interval: str = "1h",
227
234
  ) -> TradingSessionResult | None:
228
235
  try:
229
- # Create metric emitter if configured
230
236
  emitter = None
231
- if emission is not None:
232
- emitter = create_metric_emitters(emission, setup.name)
233
-
237
+ emitter_data = None
238
+ if enable_inmemory_emitter:
239
+ emitter = InMemoryMetricEmitter(stats_interval=emitter_stats_interval)
234
240
  runner = SimulationRunner(
235
241
  setup=setup,
236
242
  data_config=data_setup,
@@ -258,6 +264,9 @@ def _run_setup(
258
264
  # Filter out None values to match TradingSessionResult expected type
259
265
  commissions_for_result = {k: v for k, v in commissions_for_result.items() if v is not None}
260
266
 
267
+ if enable_inmemory_emitter and emitter is not None:
268
+ emitter_data = emitter.get_dataframe()
269
+
261
270
  return TradingSessionResult(
262
271
  setup_id,
263
272
  setup.name,
@@ -276,6 +285,7 @@ def _run_setup(
276
285
  parameters=runner.strategy_params,
277
286
  is_simulation=True,
278
287
  author=get_current_user(),
288
+ emitter_data=emitter_data,
279
289
  )
280
290
  except Exception as e:
281
291
  logger.error(f"Simulation setup {setup_id} failed with error: {e}")
@@ -213,38 +213,81 @@ class SignalsProxy(IStrategy):
213
213
  return None
214
214
 
215
215
 
216
+ def _process_single_symbol_or_instrument(
217
+ symbol_or_instrument: SymbolOrInstrument_t,
218
+ default_exchange: ExchangeName_t | None,
219
+ requested_exchange: ExchangeName_t | None,
220
+ ) -> tuple[Instrument | None, str | None]:
221
+ """
222
+ Process a single symbol or instrument and return the resolved instrument and exchange.
223
+
224
+ Returns:
225
+ tuple[Instrument | None, str | None]: (instrument, exchange) or (None, None) if processing failed
226
+ """
227
+ match symbol_or_instrument:
228
+ case str():
229
+ _e, _s = (
230
+ symbol_or_instrument.split(":")
231
+ if ":" in symbol_or_instrument
232
+ else (default_exchange, symbol_or_instrument)
233
+ )
234
+
235
+ if _e is None:
236
+ logger.warning(
237
+ f"Can't extract exchange name from symbol's spec ({symbol_or_instrument}) and exact exchange name is not provided - skip this symbol !"
238
+ )
239
+ return None, None
240
+
241
+ if (
242
+ requested_exchange is not None
243
+ and isinstance(requested_exchange, str)
244
+ and _e.lower() != requested_exchange.lower()
245
+ ):
246
+ logger.warning(
247
+ f"Exchange from symbol's spec ({_e}) is different from requested: {requested_exchange} !"
248
+ )
249
+
250
+ if (instrument := lookup.find_symbol(_e, _s)) is not None:
251
+ return instrument, _e.upper()
252
+ else:
253
+ logger.warning(f"Can't find instrument for specified symbol ({symbol_or_instrument}) - ignoring !")
254
+ return None, None
255
+
256
+ case Instrument():
257
+ return symbol_or_instrument, symbol_or_instrument.exchange
258
+
259
+ case _:
260
+ raise SimulationConfigError(
261
+ f"Unsupported type for {symbol_or_instrument} only str or Instrument instances are allowed!"
262
+ )
263
+
264
+
216
265
  def find_instruments_and_exchanges(
217
266
  instruments: list[SymbolOrInstrument_t] | dict[ExchangeName_t, list[SymbolOrInstrument_t]],
218
- exchange: ExchangeName_t | None,
267
+ exchange: ExchangeName_t | list[ExchangeName_t] | None,
219
268
  ) -> tuple[list[Instrument], list[ExchangeName_t]]:
220
269
  _instrs: list[Instrument] = []
221
- _exchanges = [] if exchange is None else [exchange]
222
- for i in instruments:
223
- match i:
224
- case str():
225
- _e, _s = i.split(":") if ":" in i else (exchange, i)
226
- assert _e is not None
270
+ _exchanges = [] if exchange is None else [exchange] if isinstance(exchange, str) else exchange
227
271
 
228
- if exchange is not None and _e.lower() != exchange.lower():
229
- logger.warning("Exchange from symbol's spec ({_e}) is different from requested: {exchange} !")
272
+ # Handle dictionary case where instruments is {exchange: [symbols]}
273
+ if isinstance(instruments, dict):
274
+ for exchange_name, symbol_list in instruments.items():
275
+ if exchange_name not in _exchanges:
276
+ _exchanges.append(exchange_name)
230
277
 
231
- if _e is None:
232
- logger.warning(
233
- "Can't extract exchange name from symbol's spec ({_e}) and exact exchange name is not provided - skip this symbol !"
234
- )
235
-
236
- if (ix := lookup.find_symbol(_e, _s)) is not None:
237
- _exchanges.append(_e.upper())
238
- _instrs.append(ix)
239
- else:
240
- logger.warning(f"Can't find instrument for specified symbol ({i}) - ignoring !")
278
+ for symbol in symbol_list:
279
+ instrument, resolved_exchange = _process_single_symbol_or_instrument(symbol, exchange_name, exchange)
280
+ if instrument is not None and resolved_exchange is not None:
281
+ _instrs.append(instrument)
282
+ _exchanges.append(resolved_exchange)
241
283
 
242
- case Instrument():
243
- _exchanges.append(i.exchange)
244
- _instrs.append(i)
245
-
246
- case _:
247
- raise SimulationConfigError(f"Unsupported type for {i} only str or Instrument instances are allowed!")
284
+ # Handle list case
285
+ else:
286
+ for symbol in instruments:
287
+ instrument, resolved_exchange = _process_single_symbol_or_instrument(symbol, exchange, exchange)
288
+ if instrument is not None and resolved_exchange is not None:
289
+ _instrs.append(instrument)
290
+ _exchanges.append(resolved_exchange)
248
291
 
249
292
  return _instrs, list(set(_exchanges))
250
293
 
@@ -7,6 +7,7 @@ from qubx.core.basics import (
7
7
  ZERO_COSTS,
8
8
  AssetBalance,
9
9
  Deal,
10
+ FundingPayment,
10
11
  Instrument,
11
12
  ITimeProvider,
12
13
  Order,
@@ -104,18 +105,23 @@ class BasicAccountProcessor(IAccountProcessor):
104
105
  ########################################################
105
106
  def get_leverage(self, instrument: Instrument) -> float:
106
107
  pos = self._positions.get(instrument)
108
+ capital = self.get_total_capital()
109
+ if np.isclose(capital, 0):
110
+ return 0.0
107
111
  if pos is not None:
108
- return pos.notional_value / self.get_total_capital()
112
+ return pos.notional_value / capital
109
113
  return 0.0
110
114
 
111
115
  def get_leverages(self, exchange: str | None = None) -> dict[Instrument, float]:
112
116
  return {s: self.get_leverage(s) for s in self._positions.keys()}
113
117
 
114
118
  def get_net_leverage(self, exchange: str | None = None) -> float:
115
- return sum(self.get_leverages(exchange).values())
119
+ leverages = self.get_leverages(exchange).values()
120
+ return sum(lev for lev in leverages if lev is not None and not np.isnan(lev))
116
121
 
117
122
  def get_gross_leverage(self, exchange: str | None = None) -> float:
118
- return sum(map(abs, self.get_leverages(exchange).values()))
123
+ leverages = self.get_leverages(exchange).values()
124
+ return sum(abs(lev) for lev in leverages if lev is not None and not np.isnan(lev))
119
125
 
120
126
  ########################################################
121
127
  # Margin information
@@ -232,6 +238,36 @@ class BasicAccountProcessor(IAccountProcessor):
232
238
  self._balances[self.base_currency] -= fee_in_base
233
239
  self._balances[instrument.settle] += realized_pnl
234
240
 
241
+ def process_funding_payment(self, instrument: Instrument, funding_payment: FundingPayment) -> None:
242
+ """Process funding payment for an instrument.
243
+
244
+ Args:
245
+ instrument: Instrument the funding payment applies to
246
+ funding_payment: Funding payment event to process
247
+ """
248
+ pos = self._positions.get(instrument)
249
+
250
+ if pos is None or not instrument.is_futures():
251
+ return
252
+
253
+ # Get current market price for funding calculation
254
+ # We need to get the mark price from the market data, but since we don't have access
255
+ # to market data here, we'll use the current position price as a reasonable fallback
256
+ mark_price = pos.position_avg_price_funds if pos.position_avg_price_funds > 0 else 0.0
257
+
258
+ # Apply funding payment to position
259
+ funding_amount = pos.apply_funding_payment(funding_payment, mark_price)
260
+
261
+ # Update account balance with funding payment
262
+ # For futures contracts, funding affects the settlement currency balance
263
+ self._balances[instrument.settle] += funding_amount
264
+
265
+ logger.debug(
266
+ f" [<y>{self.__class__.__name__}</y>(<g>{instrument}</g>)] :: "
267
+ f"funding payment {funding_amount:.6f} {instrument.settle} "
268
+ f"(rate: {funding_payment.funding_rate:.6f})"
269
+ )
270
+
235
271
  def _fill_missing_fee_info(self, instrument: Instrument, deals: list[Deal]) -> None:
236
272
  for d in deals:
237
273
  if d.fee_amount is None:
@@ -352,16 +388,48 @@ class CompositeAccountProcessor(IAccountProcessor):
352
388
  return self._account_processors[exch].get_capital()
353
389
 
354
390
  def get_total_capital(self, exchange: str | None = None) -> float:
355
- exch = self._get_exchange(exchange)
356
- return self._account_processors[exch].get_total_capital()
391
+ if exchange is not None:
392
+ # Return total capital from specific exchange
393
+ exch = self._get_exchange(exchange)
394
+ return self._account_processors[exch].get_total_capital()
395
+
396
+ # Return aggregated total capital from all exchanges when no exchange is specified
397
+ total_capital = 0.0
398
+ for exch_name, processor in self._account_processors.items():
399
+ total_capital += processor.get_total_capital()
400
+ return total_capital
357
401
 
358
402
  def get_balances(self, exchange: str | None = None) -> dict[str, AssetBalance]:
359
- exch = self._get_exchange(exchange)
360
- return self._account_processors[exch].get_balances()
403
+ if exchange is not None:
404
+ # Return balances from specific exchange
405
+ exch = self._get_exchange(exchange)
406
+ return self._account_processors[exch].get_balances()
407
+
408
+ # Return aggregated balances from all exchanges when no exchange is specified
409
+ all_balances: dict[str, AssetBalance] = defaultdict(lambda: AssetBalance())
410
+ for exch_name, processor in self._account_processors.items():
411
+ exch_balances = processor.get_balances()
412
+ for currency, balance in exch_balances.items():
413
+ if currency not in all_balances:
414
+ all_balances[currency] = AssetBalance(balance.free, balance.locked, balance.total)
415
+ else:
416
+ all_balances[currency].free += balance.free
417
+ all_balances[currency].locked += balance.locked
418
+ all_balances[currency].total += balance.total
419
+ return dict(all_balances)
361
420
 
362
421
  def get_positions(self, exchange: str | None = None) -> dict[Instrument, Position]:
363
- exch = self._get_exchange(exchange)
364
- return self._account_processors[exch].get_positions()
422
+ if exchange is not None:
423
+ # Return positions from specific exchange
424
+ exch = self._get_exchange(exchange)
425
+ return self._account_processors[exch].get_positions()
426
+
427
+ # Return positions from all exchanges when no exchange is specified
428
+ all_positions: dict[Instrument, Position] = {}
429
+ for exch_name, processor in self._account_processors.items():
430
+ exch_positions = processor.get_positions()
431
+ all_positions.update(exch_positions)
432
+ return all_positions
365
433
 
366
434
  def get_position(self, instrument: Instrument) -> Position:
367
435
  exch = self._get_exchange(instrument=instrument)
@@ -453,3 +521,7 @@ class CompositeAccountProcessor(IAccountProcessor):
453
521
  def process_deals(self, instrument: Instrument, deals: list[Deal]) -> None:
454
522
  exch = self._get_exchange(instrument=instrument)
455
523
  self._account_processors[exch].process_deals(instrument, deals)
524
+
525
+ def process_funding_payment(self, instrument: Instrument, funding_payment: FundingPayment) -> None:
526
+ exch = self._get_exchange(instrument=instrument)
527
+ self._account_processors[exch].process_funding_payment(instrument, funding_payment)