Qubx 0.6.93__tar.gz → 0.6.95__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 (263) hide show
  1. {qubx-0.6.93 → qubx-0.6.95}/PKG-INFO +1 -1
  2. {qubx-0.6.93 → qubx-0.6.95}/pyproject.toml +1 -1
  3. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/backtester/simulator.py +15 -1
  4. qubx-0.6.95/src/qubx/backtester/transfers.py +146 -0
  5. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/xlighter/account.py +127 -237
  6. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/xlighter/constants.py +4 -3
  7. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/xlighter/data.py +227 -96
  8. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/xlighter/factory.py +5 -50
  9. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/xlighter/handlers/__init__.py +2 -0
  10. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/xlighter/handlers/base.py +12 -0
  11. qubx-0.6.95/src/qubx/connectors/xlighter/handlers/orderbook.py +128 -0
  12. qubx-0.6.95/src/qubx/connectors/xlighter/handlers/stats.py +351 -0
  13. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/xlighter/parsers.py +2 -21
  14. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/xlighter/reader.py +2 -1
  15. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/xlighter/websocket.py +216 -113
  16. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/core/account.py +15 -12
  17. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/core/basics.py +37 -6
  18. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/core/context.py +71 -6
  19. qubx-0.6.95/src/qubx/core/detectors/__init__.py +4 -0
  20. qubx-0.6.95/src/qubx/core/detectors/delisting.py +81 -0
  21. qubx-0.6.93/src/qubx/core/stale_data_detector.py → qubx-0.6.95/src/qubx/core/detectors/stale.py +101 -100
  22. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/core/initializer.py +46 -1
  23. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/core/interfaces.py +88 -2
  24. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/core/metrics.py +107 -10
  25. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/core/mixins/processing.py +13 -17
  26. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/core/mixins/trading.py +89 -5
  27. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/core/mixins/universe.py +7 -0
  28. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/data/storages/questdb.py +16 -2
  29. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/misc.py +6 -1
  30. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/orderbook.py +277 -2
  31. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/runner/configs.py +14 -7
  32. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/runner/runner.py +8 -5
  33. qubx-0.6.95/src/qubx/utils/websocket_manager.py +445 -0
  34. qubx-0.6.93/src/qubx/connectors/xlighter/handlers/orderbook.py +0 -207
  35. qubx-0.6.93/src/qubx/connectors/xlighter/orderbook_maintainer.py +0 -314
  36. qubx-0.6.93/src/qubx/utils/websocket_manager.py +0 -442
  37. {qubx-0.6.93 → qubx-0.6.95}/LICENSE +0 -0
  38. {qubx-0.6.93 → qubx-0.6.95}/README.md +0 -0
  39. {qubx-0.6.93 → qubx-0.6.95}/build.py +0 -0
  40. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/__init__.py +0 -0
  41. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/_nb_magic.py +0 -0
  42. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/backtester/__init__.py +0 -0
  43. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/backtester/account.py +0 -0
  44. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/backtester/broker.py +0 -0
  45. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/backtester/data.py +0 -0
  46. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/backtester/management.py +0 -0
  47. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/backtester/ome.py +0 -0
  48. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/backtester/optimization.py +0 -0
  49. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/backtester/runner.py +0 -0
  50. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/backtester/sentinels.py +0 -0
  51. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/backtester/simulated_data.py +0 -0
  52. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/backtester/simulated_exchange.py +0 -0
  53. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/backtester/utils.py +0 -0
  54. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/cli/__init__.py +0 -0
  55. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/cli/commands.py +0 -0
  56. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/cli/deploy.py +0 -0
  57. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/cli/misc.py +0 -0
  58. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/cli/release.py +0 -0
  59. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/cli/tui.py +0 -0
  60. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/__init__.py +0 -0
  61. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/account.py +0 -0
  62. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/adapters/__init__.py +0 -0
  63. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/adapters/polling_adapter.py +0 -0
  64. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/broker.py +0 -0
  65. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/connection_manager.py +0 -0
  66. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/data.py +0 -0
  67. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  68. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchange_manager.py +0 -0
  69. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/__init__.py +0 -0
  70. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/base.py +0 -0
  71. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/binance/broker.py +0 -0
  72. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/binance/exchange.py +0 -0
  73. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex.py +0 -0
  74. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/bitfinex/bitfinex_account.py +0 -0
  75. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/hyperliquid/__init__.py +0 -0
  76. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/hyperliquid/account.py +0 -0
  77. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/hyperliquid/broker.py +0 -0
  78. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/hyperliquid/hyperliquid.py +0 -0
  79. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/exchanges/kraken/kraken.py +0 -0
  80. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/factory.py +0 -0
  81. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/__init__.py +0 -0
  82. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/base.py +0 -0
  83. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/factory.py +0 -0
  84. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/funding_rate.py +0 -0
  85. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/liquidation.py +0 -0
  86. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/ohlc.py +0 -0
  87. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/open_interest.py +0 -0
  88. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/orderbook.py +0 -0
  89. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/quote.py +0 -0
  90. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/handlers/trade.py +0 -0
  91. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/reader.py +0 -0
  92. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/subscription_config.py +0 -0
  93. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/subscription_manager.py +0 -0
  94. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/subscription_orchestrator.py +0 -0
  95. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/utils.py +0 -0
  96. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/ccxt/warmup_service.py +0 -0
  97. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/tardis/data.py +0 -0
  98. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/tardis/utils.py +0 -0
  99. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/xlighter/__init__.py +0 -0
  100. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/xlighter/broker.py +0 -0
  101. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/xlighter/client.py +0 -0
  102. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/xlighter/extensions.py +0 -0
  103. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/xlighter/handlers/quote.py +0 -0
  104. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/xlighter/handlers/trades.py +0 -0
  105. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/xlighter/instruments.py +0 -0
  106. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/connectors/xlighter/utils.py +0 -0
  107. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/core/__init__.py +0 -0
  108. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/core/deque.py +0 -0
  109. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/core/errors.py +0 -0
  110. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/core/exceptions.py +0 -0
  111. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/core/helpers.py +0 -0
  112. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/core/loggers.py +0 -0
  113. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/core/lookups.py +0 -0
  114. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/core/mixins/__init__.py +0 -0
  115. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/core/mixins/market.py +0 -0
  116. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/core/mixins/subscription.py +0 -0
  117. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/core/mixins/utils.py +0 -0
  118. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/core/series.pxd +0 -0
  119. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/core/series.pyi +0 -0
  120. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/core/series.pyx +0 -0
  121. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/core/utils.pyi +0 -0
  122. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/core/utils.pyx +0 -0
  123. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/data/__init__.py +0 -0
  124. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/data/composite.py +0 -0
  125. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/data/containers.py +0 -0
  126. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/data/helpers.py +0 -0
  127. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/data/hft.py +0 -0
  128. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/data/readers.py +0 -0
  129. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/data/registry.py +0 -0
  130. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/data/storage.py +0 -0
  131. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/data/storages/csv.py +0 -0
  132. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/data/storages/utils.py +0 -0
  133. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/data/tardis.py +0 -0
  134. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/data/transformers.py +0 -0
  135. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/emitters/__init__.py +0 -0
  136. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/emitters/base.py +0 -0
  137. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/emitters/composite.py +0 -0
  138. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/emitters/csv.py +0 -0
  139. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/emitters/indicator.py +0 -0
  140. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/emitters/inmemory.py +0 -0
  141. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/emitters/prometheus.py +0 -0
  142. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/emitters/questdb.py +0 -0
  143. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/exporters/__init__.py +0 -0
  144. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/exporters/composite.py +0 -0
  145. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/exporters/formatters/__init__.py +0 -0
  146. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/exporters/formatters/base.py +0 -0
  147. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/exporters/formatters/incremental.py +0 -0
  148. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/exporters/formatters/slack.py +0 -0
  149. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/exporters/formatters/target_position.py +0 -0
  150. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/exporters/redis_streams.py +0 -0
  151. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/exporters/slack.py +0 -0
  152. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/features/__init__.py +0 -0
  153. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/features/core.py +0 -0
  154. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/features/orderbook.py +0 -0
  155. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/features/price.py +0 -0
  156. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/features/trades.py +0 -0
  157. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/features/utils.py +0 -0
  158. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/gathering/simplest.py +0 -0
  159. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/health/__init__.py +0 -0
  160. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/health/base.py +0 -0
  161. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/loggers/__init__.py +0 -0
  162. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/loggers/csv.py +0 -0
  163. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/loggers/factory.py +0 -0
  164. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/loggers/inmemory.py +0 -0
  165. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/loggers/mongo.py +0 -0
  166. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/math/__init__.py +0 -0
  167. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/math/stats.py +0 -0
  168. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/notifications/__init__.py +0 -0
  169. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/notifications/composite.py +0 -0
  170. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/notifications/slack.py +0 -0
  171. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/notifications/throttler.py +0 -0
  172. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/pandaz/__init__.py +0 -0
  173. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/pandaz/ta.py +0 -0
  174. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/pandaz/utils.py +0 -0
  175. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/resources/_build.py +0 -0
  176. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/resources/crypto-fees.ini +0 -0
  177. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/resources/instruments/hyperliquid-spot.json +0 -0
  178. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/resources/instruments/hyperliquid.f-perpetual.json +0 -0
  179. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-binance-spot.json +0 -0
  180. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-binance.cm-future.json +0 -0
  181. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-binance.cm-perpetual.json +0 -0
  182. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-binance.um-future.json +0 -0
  183. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-binance.um-perpetual.json +0 -0
  184. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-bitfinex.f-perpetual.json +0 -0
  185. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-kraken-spot.json +0 -0
  186. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-kraken.f-future.json +0 -0
  187. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/resources/instruments/symbols-kraken.f-perpetual.json +0 -0
  188. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/restarts/__init__.py +0 -0
  189. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/restarts/state_resolvers.py +0 -0
  190. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/restarts/time_finders.py +0 -0
  191. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/restorers/__init__.py +0 -0
  192. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/restorers/balance.py +0 -0
  193. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/restorers/factory.py +0 -0
  194. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/restorers/interfaces.py +0 -0
  195. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/restorers/position.py +0 -0
  196. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/restorers/signal.py +0 -0
  197. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/restorers/state.py +0 -0
  198. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/restorers/utils.py +0 -0
  199. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/ta/__init__.py +0 -0
  200. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/ta/indicators.pxd +0 -0
  201. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/ta/indicators.pyi +0 -0
  202. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/ta/indicators.pyx +0 -0
  203. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/templates/__init__.py +0 -0
  204. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/templates/base.py +0 -0
  205. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/templates/project/accounts.toml.j2 +0 -0
  206. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/templates/project/config.yml.j2 +0 -0
  207. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/templates/project/jlive.sh.j2 +0 -0
  208. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/templates/project/jpaper.sh.j2 +0 -0
  209. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/templates/project/pyproject.toml.j2 +0 -0
  210. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/templates/project/src/{{ strategy_name }}/__init__.py.j2 +0 -0
  211. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/templates/project/src/{{ strategy_name }}/strategy.py.j2 +0 -0
  212. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/templates/project/template.yml +0 -0
  213. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/templates/simple/__init__.py.j2 +0 -0
  214. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/templates/simple/accounts.toml.j2 +0 -0
  215. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/templates/simple/config.yml.j2 +0 -0
  216. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/templates/simple/jlive.sh.j2 +0 -0
  217. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/templates/simple/jpaper.sh.j2 +0 -0
  218. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/templates/simple/strategy.py.j2 +0 -0
  219. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/templates/simple/template.yml +0 -0
  220. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/trackers/__init__.py +0 -0
  221. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/trackers/advanced.py +0 -0
  222. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/trackers/composite.py +0 -0
  223. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/trackers/rebalancers.py +0 -0
  224. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/trackers/riskctrl.py +0 -0
  225. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/trackers/sizers.py +0 -0
  226. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/__init__.py +0 -0
  227. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/_pyxreloader.py +0 -0
  228. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/charting/lookinglass.py +0 -0
  229. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  230. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/charting/orderbook.py +0 -0
  231. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/collections.py +0 -0
  232. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/marketdata/binance.py +0 -0
  233. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/marketdata/ccxt.py +0 -0
  234. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/marketdata/dukas.py +0 -0
  235. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/ntp.py +0 -0
  236. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/numbers_utils.py +0 -0
  237. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/plotting/__init__.py +0 -0
  238. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/plotting/dashboard.py +0 -0
  239. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/plotting/data.py +0 -0
  240. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/plotting/interfaces.py +0 -0
  241. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  242. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  243. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/questdb.py +0 -0
  244. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/runner/__init__.py +0 -0
  245. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
  246. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/runner/accounts.py +0 -0
  247. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/runner/factory.py +0 -0
  248. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/runner/kernel_service.py +0 -0
  249. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/runner/textual/__init__.py +0 -0
  250. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/runner/textual/app.py +0 -0
  251. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/runner/textual/handlers.py +0 -0
  252. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/runner/textual/init_code.py +0 -0
  253. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/runner/textual/kernel.py +0 -0
  254. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/runner/textual/styles.tcss +0 -0
  255. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/runner/textual/widgets/__init__.py +0 -0
  256. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/runner/textual/widgets/command_input.py +0 -0
  257. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/runner/textual/widgets/debug_log.py +0 -0
  258. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/runner/textual/widgets/orders_table.py +0 -0
  259. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/runner/textual/widgets/positions_table.py +0 -0
  260. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/runner/textual/widgets/quotes_table.py +0 -0
  261. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/runner/textual/widgets/repl_output.py +0 -0
  262. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/time.py +0 -0
  263. {qubx-0.6.93 → qubx-0.6.95}/src/qubx/utils/version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Qubx
3
- Version: 0.6.93
3
+ Version: 0.6.95
4
4
  Summary: Qubx - Quantitative Trading Framework
5
5
  License-File: LICENSE
6
6
  Author: Dmitry Marienko
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "Qubx"
7
- version = "0.6.93"
7
+ version = "0.6.95"
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,4 +1,4 @@
1
- from typing import Literal
1
+ from typing import Literal, cast
2
2
 
3
3
  import pandas as pd
4
4
  from joblib import delayed
@@ -15,6 +15,7 @@ from qubx.utils.runner.configs import PrefetchConfig
15
15
  from qubx.utils.time import handle_start_stop, to_utc_naive
16
16
 
17
17
  from .runner import SimulationRunner
18
+ from .transfers import SimulationTransferManager
18
19
  from .utils import (
19
20
  DataDecls_t,
20
21
  ExchangeName_t,
@@ -300,6 +301,18 @@ def _run_setup(
300
301
  if enable_inmemory_emitter and emitter is not None:
301
302
  emitter_data = emitter.get_dataframe()
302
303
 
304
+ # - get transfers log
305
+ transfers_log = None
306
+ if hasattr(runner.ctx, "_transfer_manager") and isinstance(
307
+ getattr(runner.ctx, "_transfer_manager"), SimulationTransferManager
308
+ ):
309
+ try:
310
+ transfer_manager = cast(SimulationTransferManager, getattr(runner.ctx, "_transfer_manager"))
311
+ transfers_log = transfer_manager.get_transfers_dataframe()
312
+ except Exception as e:
313
+ logger.error(f"Failed to get transfers log: {e}")
314
+ transfers_log = None
315
+
303
316
  return TradingSessionResult(
304
317
  setup_id,
305
318
  setup.name,
@@ -319,6 +332,7 @@ def _run_setup(
319
332
  is_simulation=True,
320
333
  author=get_current_user(),
321
334
  emitter_data=emitter_data,
335
+ transfers_log=transfers_log,
322
336
  )
323
337
  except Exception as e:
324
338
  logger.error(f"Simulation setup {setup_id} failed with error: {e}")
@@ -0,0 +1,146 @@
1
+ import uuid
2
+ from typing import Any
3
+
4
+ import pandas as pd
5
+
6
+ from qubx import logger
7
+ from qubx.core.account import CompositeAccountProcessor
8
+ from qubx.core.basics import ITimeProvider
9
+ from qubx.core.interfaces import ITransferManager
10
+
11
+
12
+ class SimulationTransferManager(ITransferManager):
13
+ """
14
+ Transfer manager for simulation mode.
15
+
16
+ Handles fund transfers between exchanges by directly manipulating account balances.
17
+ All transfers are instant and tracked in a DataFrame for export to results.
18
+ """
19
+
20
+ _account: CompositeAccountProcessor
21
+ _time: ITimeProvider
22
+ _transfers: list[dict[str, Any]]
23
+
24
+ def __init__(self, account_processor: CompositeAccountProcessor, time_provider: ITimeProvider):
25
+ """
26
+ Initialize simulation transfer manager.
27
+
28
+ Args:
29
+ account_processor: Account processor (typically CompositeAccountProcessor)
30
+ time_provider: Time provider for timestamping transfers
31
+ """
32
+ self._account = account_processor
33
+ self._time = time_provider
34
+ self._transfers = []
35
+
36
+ def transfer_funds(self, from_exchange: str, to_exchange: str, currency: str, amount: float) -> str:
37
+ """
38
+ Transfer funds between exchanges (instant in simulation).
39
+
40
+ Args:
41
+ from_exchange: Source exchange identifier
42
+ to_exchange: Destination exchange identifier
43
+ currency: Currency to transfer
44
+ amount: Amount to transfer
45
+
46
+ Returns:
47
+ str: Transaction ID (UUID)
48
+
49
+ Raises:
50
+ ValueError: If exchanges not found or insufficient funds
51
+ """
52
+ # Generate transaction ID
53
+ transaction_id = f"sim_{uuid.uuid4().hex[:12]}"
54
+
55
+ # Get timestamp
56
+ timestamp = self._time.time()
57
+
58
+ # Get individual processors
59
+ try:
60
+ from_processor = self._account.get_account_processor(from_exchange)
61
+ to_processor = self._account.get_account_processor(to_exchange)
62
+ except (KeyError, AttributeError) as e:
63
+ raise ValueError(f"Exchange not found: {e}")
64
+
65
+ # Validate sufficient funds
66
+ from_balances = from_processor.get_balances()
67
+ if currency not in from_balances:
68
+ raise ValueError(f"Currency '{currency}' not found in {from_exchange}")
69
+
70
+ available = from_balances[currency].free
71
+ if available < amount:
72
+ raise ValueError(
73
+ f"Insufficient funds in {from_exchange}: "
74
+ f"{available:.8f} {currency} available, {amount:.8f} {currency} requested"
75
+ )
76
+
77
+ # Execute transfer (instant balance manipulation)
78
+ from_balances[currency].total -= amount
79
+ from_balances[currency].free -= amount
80
+
81
+ to_balances = to_processor.get_balances()
82
+ to_balances[currency].total += amount
83
+ to_balances[currency].free += amount
84
+
85
+ # Record transfer
86
+ transfer_record = {
87
+ "transaction_id": transaction_id,
88
+ "timestamp": timestamp,
89
+ "from_exchange": from_exchange,
90
+ "to_exchange": to_exchange,
91
+ "currency": currency,
92
+ "amount": amount,
93
+ "status": "completed", # Always completed in simulation
94
+ }
95
+ self._transfers.append(transfer_record)
96
+
97
+ logger.debug(f"[SimTransfer] {amount:.8f} {currency} {from_exchange} → {to_exchange} (ID: {transaction_id})")
98
+
99
+ return transaction_id
100
+
101
+ def get_transfer_status(self, transaction_id: str) -> dict[str, Any]:
102
+ """
103
+ Get the status of a transfer.
104
+
105
+ Args:
106
+ transaction_id: Transaction ID
107
+
108
+ Returns:
109
+ dict[str, Any]: Transfer status information
110
+ """
111
+ # Find transfer
112
+ for transfer in self._transfers:
113
+ if transfer["transaction_id"] == transaction_id:
114
+ return transfer.copy()
115
+
116
+ # Not found
117
+ return {
118
+ "transaction_id": transaction_id,
119
+ "status": "not_found",
120
+ "error": f"Transaction {transaction_id} not found",
121
+ }
122
+
123
+ def get_transfers(self) -> dict[str, dict[str, Any]]:
124
+ """
125
+ Get all transfers as a dictionary.
126
+
127
+ Returns:
128
+ dict[str, dict[str, Any]]: Dictionary mapping transaction IDs to transfer info
129
+ """
130
+ return {t["transaction_id"]: t for t in self._transfers}
131
+
132
+ def get_transfers_dataframe(self) -> pd.DataFrame:
133
+ """
134
+ Get all transfers as a pandas DataFrame.
135
+
136
+ Returns:
137
+ pd.DataFrame: DataFrame with columns [transaction_id, timestamp, from_exchange,
138
+ to_exchange, currency, amount, status]
139
+ """
140
+ if not self._transfers:
141
+ # Return empty DataFrame with correct schema
142
+ return pd.DataFrame(
143
+ columns=["transaction_id", "from_exchange", "to_exchange", "currency", "amount", "status"] # type: ignore
144
+ )
145
+
146
+ return pd.DataFrame(self._transfers).set_index("timestamp")
@@ -19,6 +19,7 @@ import numpy as np
19
19
  from qubx import logger
20
20
  from qubx.core.account import BasicAccountProcessor
21
21
  from qubx.core.basics import (
22
+ ZERO_COSTS,
22
23
  CtrlChannel,
23
24
  DataType,
24
25
  Deal,
@@ -65,7 +66,7 @@ class LighterAccountProcessor(BasicAccountProcessor):
65
66
  time_provider: ITimeProvider,
66
67
  loop: asyncio.AbstractEventLoop,
67
68
  base_currency: str = "USDC",
68
- tcc: TransactionCostsCalculator = None,
69
+ tcc: TransactionCostsCalculator | None = None,
69
70
  initial_capital: float = 100_000,
70
71
  max_retries: int = 10,
71
72
  connection_timeout: int = 30,
@@ -88,8 +89,6 @@ class LighterAccountProcessor(BasicAccountProcessor):
88
89
  connection_timeout: Connection timeout in seconds
89
90
  """
90
91
  if tcc is None:
91
- from qubx.core.basics import ZERO_COSTS
92
-
93
92
  tcc = ZERO_COSTS
94
93
 
95
94
  super().__init__(
@@ -121,12 +120,20 @@ class LighterAccountProcessor(BasicAccountProcessor):
121
120
  self._auth_token_expiry: Optional[int] = None
122
121
  self._processed_tx_hashes: set[str] = set() # Track processed transaction hashes
123
122
 
123
+ self._account_stats_initialized = False
124
+
124
125
  logger.info(f"Initialized LighterAccountProcessor for account {account_id}")
125
126
 
126
127
  def set_subscription_manager(self, manager: ISubscriptionManager) -> None:
127
128
  """Set the subscription manager (required by interface)"""
128
129
  self._subscription_manager = manager
129
130
 
131
+ def get_total_capital(self, exchange: str | None = None) -> float:
132
+ if not self._account_stats_initialized:
133
+ self._async_loop.submit(self._start_subscriptions())
134
+ self._wait_for_account_stats_initialized()
135
+ return super().get_total_capital(exchange)
136
+
130
137
  def start(self):
131
138
  """Start WebSocket subscriptions for account data"""
132
139
  if self._is_running:
@@ -142,33 +149,13 @@ class LighterAccountProcessor(BasicAccountProcessor):
142
149
  # Start subscription tasks using AsyncThreadLoop
143
150
  logger.info("Starting Lighter account subscriptions")
144
151
 
145
- # Submit connection and subscription tasks to the event loop
146
- self._async_loop.submit(self._start_subscriptions())
152
+ if not self._account_stats_initialized:
153
+ # Submit connection and subscription tasks to the event loop
154
+ self._async_loop.submit(self._start_subscriptions())
155
+ self._wait_for_account_stats_initialized()
147
156
 
148
157
  logger.info("Lighter account subscriptions started")
149
158
 
150
- async def _start_subscriptions(self):
151
- """Connect to WebSocket and start all subscriptions"""
152
- try:
153
- # Ensure WebSocket is connected
154
- if not self.ws_manager.is_connected:
155
- logger.info("Connecting to Lighter WebSocket...")
156
- await self.ws_manager.connect()
157
- logger.info("Connected to Lighter WebSocket")
158
-
159
- # Generate auth token for authenticated channels
160
- await self._generate_auth_token()
161
-
162
- # Start all subscriptions
163
- await self._subscribe_account_all()
164
- await self._subscribe_account_all_orders()
165
- await self._subscribe_user_stats()
166
-
167
- except Exception as e:
168
- logger.error(f"Failed to start subscriptions: {e}")
169
- self._is_running = False
170
- raise
171
-
172
159
  def stop(self):
173
160
  """Stop all WebSocket subscriptions"""
174
161
  if not self._is_running:
@@ -183,164 +170,134 @@ class LighterAccountProcessor(BasicAccountProcessor):
183
170
 
184
171
  self._subscription_tasks.clear()
185
172
  self._is_running = False
186
-
187
173
  logger.info("Lighter account subscriptions stopped")
188
174
 
189
- async def _generate_auth_token(self):
175
+ def process_deals(self, instrument: Instrument, deals: list[Deal], is_snapshot: bool = False) -> None:
190
176
  """
191
- Generate authentication token for WebSocket subscriptions.
177
+ Override process_deals to track fees WITHOUT updating positions.
178
+
179
+ In Lighter, positions are synced directly from account_all channel
180
+ (single source of truth for quantity and avg_entry_price). However,
181
+ we still need to track fees/commissions from deals.
192
182
 
193
- Auth tokens are required for account-specific channels:
194
- - account_all/{account_id}
195
- - account_all_orders/{account_id}
196
- - user_stats/{account_id}
183
+ This prevents double position updates while ensuring commission tracking.
197
184
 
198
- Note: create_auth_token_with_expiry() returns (auth_token_string, error_string)
199
- where auth_token_string is the token itself (not an object).
200
- Default expiry is 10 minutes from creation time.
185
+ Args:
186
+ instrument: The instrument for the deals
187
+ deals: List of Deal objects
188
+ is_snapshot: Whether this is a snapshot or incremental update
201
189
  """
202
- try:
203
- logger.info("Generating auth token...")
190
+ # Do NOT call super().process_deals() - that would update positions
191
+ # Instead, manually track fees for the position
192
+ if not deals:
193
+ return
194
+
195
+ position = self.get_position(instrument)
196
+
197
+ for deal in deals:
198
+ # Track commission from the deal
199
+ if deal.fee_amount and deal.fee_amount > 0:
200
+ # Add fee to position's commission tracking
201
+ position.commissions += deal.fee_amount
204
202
 
205
- # Use SignerClient to create auth token
206
- # Returns (token_string, error_string)
207
- signer = self.client.signer_client
208
- auth_token, error = signer.create_auth_token_with_expiry()
203
+ logger.debug(
204
+ f"Tracked fee for {instrument.symbol}: {deal.fee_amount:.6f} {deal.fee_currency} "
205
+ f"(total commissions: {position.commissions:.6f})"
206
+ )
207
+
208
+ logger.debug(
209
+ f"Processed {len(deals)} deal(s) for {instrument.symbol} - fees tracked, positions synced from account_all"
210
+ )
211
+
212
+ def process_order(self, order: Order, update_locked_value: bool = True) -> None:
213
+ """
214
+ Override process_order to handle Lighter's server-assigned order IDs.
215
+
216
+ Lighter assigns server IDs different from our client_id. When an order
217
+ update arrives with a new server ID but matching client_id, we need to
218
+ migrate the order from client_id key to server_id key while preserving
219
+ the same object instance (for external references).
220
+
221
+ Args:
222
+ order: Order update from WebSocket
223
+ update_locked_value: Whether to update locked capital tracking
224
+ """
225
+ # Check if order exists under client_id (migration case)
226
+ if order.client_id and order.client_id in self._active_orders:
227
+ # Get the existing order stored under client_id
228
+ existing_order = self._active_orders[order.client_id]
229
+
230
+ logger.debug(f"Migrating order: client_id={order.client_id} → server_id={order.id}")
231
+
232
+ # Remove from old location
233
+ self._active_orders.pop(order.client_id)
209
234
 
210
- if error:
211
- raise RuntimeError(f"Failed to generate auth token: {error}")
235
+ # Store it under the new server ID before base class processing
236
+ # This allows base class merge logic to find and update it in place
237
+ self._active_orders[order.id] = existing_order
212
238
 
213
- if not auth_token:
214
- raise RuntimeError("Auth token is empty")
239
+ # Also migrate locked capital tracking if present
240
+ if order.client_id in self._locked_capital_by_order:
241
+ locked_value = self._locked_capital_by_order.pop(order.client_id)
242
+ self._locked_capital_by_order[order.id] = locked_value
215
243
 
216
- # Store token (it's already a string, not an object)
217
- self._auth_token = auth_token
218
- # Calculate expiry (default is 10 minutes from now)
219
- self._auth_token_expiry = int(time.time()) + 10 * 60
244
+ # Let base class handle the rest (merge, store, lock/unlock, etc.)
245
+ # The base class will now find the existing order under order.id and merge in place
246
+ super().process_order(order, update_locked_value)
220
247
 
221
- logger.info(f"Auth token generated successfully (expires at: {self._auth_token_expiry})")
248
+ def _wait_for_account_stats_initialized(self):
249
+ max_wait_time = 20.0 # seconds
250
+ elapsed = 0.0
251
+ interval = 0.1
252
+ while not self._account_stats_initialized:
253
+ if elapsed >= max_wait_time:
254
+ raise TimeoutError(f"Account stats were not initialized within {max_wait_time} seconds")
255
+ time.sleep(interval)
256
+ elapsed += interval
257
+
258
+ async def _start_subscriptions(self):
259
+ """Connect to WebSocket and start all subscriptions"""
260
+ try:
261
+ # Ensure WebSocket is connected
262
+ if not self.ws_manager.is_connected:
263
+ logger.info("Connecting to Lighter WebSocket...")
264
+ await self.ws_manager.connect()
265
+ logger.info("Connected to Lighter WebSocket")
266
+
267
+ # Start all subscriptions
268
+ await self._subscribe_account_all()
269
+ await self._subscribe_account_all_orders()
270
+ await self._subscribe_user_stats()
222
271
 
223
272
  except Exception as e:
224
- logger.error(f"Failed to generate auth token: {e}")
273
+ logger.error(f"Failed to start subscriptions: {e}")
274
+ self._is_running = False
225
275
  raise
226
276
 
227
277
  async def _subscribe_account_all(self):
228
- """
229
- Subscribe to account_all channel for positions and trades (primary channel).
230
-
231
- Requires authentication token.
232
-
233
- This is the single source of truth for positions and trade history.
234
- Updates positions directly from position data, sends trades as Deals
235
- through channel for strategy notification.
236
-
237
- Message format:
238
- {
239
- "account": 225671,
240
- "channel": "account_all:225671",
241
- "type": "update/account_all",
242
- "positions": {
243
- "24": {
244
- "market_id": 24,
245
- "sign": -1, # 1 for long, -1 for short
246
- "position": "1.00",
247
- "avg_entry_price": "40.1342",
248
- ...
249
- }
250
- },
251
- "trades": {
252
- "24": [
253
- {
254
- "trade_id": 225067334,
255
- "market_id": 24,
256
- "size": "1.00",
257
- "price": "40.1342",
258
- "timestamp": 1760287839079,
259
- ...
260
- }
261
- ]
262
- },
263
- "funding_histories": {}
264
- }
265
- """
266
- channel = f"account_all/{self._lighter_account_index}"
267
- logger.info(f"Subscribing to {channel} (with auth)")
268
-
269
278
  try:
270
- # Subscribe with auth token
271
- await self.ws_manager.subscribe(
272
- channel=channel, handler=self._handle_account_all_message, auth=self._auth_token
273
- )
274
- logger.info(f"Successfully subscribed to {channel}")
279
+ await self.ws_manager.subscribe_account_all(self._lighter_account_index, self._handle_account_all_message)
280
+ logger.info(f"Subscribed to account_all for account {self._lighter_account_index}")
275
281
  except Exception as e:
276
- logger.error(f"Failed to subscribe to {channel}: {e}")
282
+ logger.error(f"Failed to subscribe to account_all for account {self._lighter_account_index}: {e}")
277
283
  raise
278
284
 
279
285
  async def _subscribe_account_all_orders(self):
280
- """
281
- Subscribe to account_all_orders channel for order updates across all markets.
282
-
283
- Requires authentication token.
284
-
285
- Message format:
286
- {
287
- "channel": "account_all_orders:225671",
288
- "type": "update/account_all_orders",
289
- "orders": {
290
- "24": [ # market_index
291
- {
292
- "order_id": "7036874567748225",
293
- "status": "filled",
294
- ...
295
- }
296
- ]
297
- }
298
- }
299
- """
300
- channel = f"account_all_orders/{self._lighter_account_index}"
301
- logger.info(f"Subscribing to {channel} (with auth)")
302
-
303
286
  try:
304
- # Subscribe with auth token
305
- await self.ws_manager.subscribe(
306
- channel=channel, handler=self._handle_account_all_orders_message, auth=self._auth_token
287
+ await self.ws_manager.subscribe_account_all_orders(
288
+ self._lighter_account_index, self._handle_account_all_orders_message
307
289
  )
308
- logger.info(f"Successfully subscribed to {channel}")
290
+ logger.info(f"Subscribed to account_all_orders for account {self._lighter_account_index}")
309
291
  except Exception as e:
310
- logger.error(f"Failed to subscribe to {channel}: {e}")
292
+ logger.error(f"Failed to subscribe to account_all_orders for account {self._lighter_account_index}: {e}")
311
293
  raise
312
294
 
313
295
  async def _subscribe_user_stats(self):
314
- """
315
- Subscribe to user_stats channel for account statistics.
316
-
317
- Requires authentication token.
318
-
319
- Message format:
320
- {
321
- "channel": "user_stats:225671",
322
- "type": "update/user_stats",
323
- "stats": {
324
- "collateral": "998.888700",
325
- "portfolio_value": "998.901500",
326
- "available_balance": "990.920600",
327
- "leverage": "0.04",
328
- "margin_usage": "0.80",
329
- ...
330
- }
331
- }
332
- """
333
- channel = f"user_stats/{self._lighter_account_index}"
334
- logger.info(f"Subscribing to {channel} (with auth)")
335
-
336
296
  try:
337
- # Subscribe with auth token
338
- await self.ws_manager.subscribe(
339
- channel=channel, handler=self._handle_user_stats_message, auth=self._auth_token
340
- )
341
- logger.info(f"Successfully subscribed to {channel}")
297
+ await self.ws_manager.subscribe_user_stats(self._lighter_account_index, self._handle_user_stats_message)
298
+ logger.info(f"Subscribed to user_stats for account {self._lighter_account_index}")
342
299
  except Exception as e:
343
- logger.error(f"Failed to subscribe to {channel}: {e}")
300
+ logger.error(f"Failed to subscribe to user_stats for account {self._lighter_account_index}: {e}")
344
301
  raise
345
302
 
346
303
  async def _handle_account_all_message(self, message: dict):
@@ -369,6 +326,8 @@ class LighterAccountProcessor(BasicAccountProcessor):
369
326
  # Sync quantity and position_avg_price from Lighter's authoritative data
370
327
  position.quantity = pos_state.quantity
371
328
  position.position_avg_price = pos_state.avg_entry_price
329
+ position.position_avg_price_funds = pos_state.avg_entry_price
330
+ position.r_pnl = pos_state.realized_pnl
372
331
 
373
332
  # Update market price for unrealized PnL recalculation
374
333
  # Use the avg_entry_price as a reference if no better price available
@@ -393,15 +352,15 @@ class LighterAccountProcessor(BasicAccountProcessor):
393
352
  f"fee={deal.fee_amount:.6f} (id={deal.id})"
394
353
  )
395
354
 
355
+ # Funding payments are handled by the data provider, so I commented them here to avoid double sending
396
356
  # Send funding payments through channel
397
- for instrument, payments in funding_payments.items():
398
- for payment in payments:
399
- # Send: (instrument, DataType.FUNDING_PAYMENT, payment, False)
400
- self.channel.send((instrument, DataType.FUNDING_PAYMENT, payment, False))
401
-
402
- logger.debug(
403
- f"Sent funding payment: {instrument.symbol} rate={payment.funding_rate:.6f} at {payment.time}"
404
- )
357
+ # for instrument, payments in funding_payments.items():
358
+ # for payment in payments:
359
+ # # Send: (instrument, DataType.FUNDING_PAYMENT, payment, False)
360
+ # self.channel.send((instrument, DataType.FUNDING_PAYMENT, payment, False))
361
+ # logger.debug(
362
+ # f"Sent funding payment: {instrument.symbol} rate={payment.funding_rate:.6f} at {payment.time}"
363
+ # )
405
364
 
406
365
  except Exception as e:
407
366
  logger.error(f"Error handling account_all message: {e}")
@@ -453,79 +412,10 @@ class LighterAccountProcessor(BasicAccountProcessor):
453
412
  f"free={balance.free:.2f}, locked={balance.locked:.2f}"
454
413
  )
455
414
 
415
+ if not self._account_stats_initialized:
416
+ self._account_stats_initialized = True
417
+ logger.debug("Account stats initialized")
418
+
456
419
  except Exception as e:
457
420
  logger.error(f"Error handling user_stats message: {e}")
458
421
  logger.exception(e)
459
-
460
- def process_deals(self, instrument: Instrument, deals: list[Deal], is_snapshot: bool = False) -> None:
461
- """
462
- Override process_deals to track fees WITHOUT updating positions.
463
-
464
- In Lighter, positions are synced directly from account_all channel
465
- (single source of truth for quantity and avg_entry_price). However,
466
- we still need to track fees/commissions from deals.
467
-
468
- This prevents double position updates while ensuring commission tracking.
469
-
470
- Args:
471
- instrument: The instrument for the deals
472
- deals: List of Deal objects
473
- is_snapshot: Whether this is a snapshot or incremental update
474
- """
475
- # Do NOT call super().process_deals() - that would update positions
476
- # Instead, manually track fees for the position
477
- if not deals:
478
- return
479
-
480
- position = self.get_position(instrument)
481
-
482
- for deal in deals:
483
- # Track commission from the deal
484
- if deal.fee_amount and deal.fee_amount > 0:
485
- # Add fee to position's commission tracking
486
- position.commissions += deal.fee_amount
487
-
488
- logger.debug(
489
- f"Tracked fee for {instrument.symbol}: {deal.fee_amount:.6f} {deal.fee_currency} "
490
- f"(total commissions: {position.commissions:.6f})"
491
- )
492
-
493
- logger.debug(
494
- f"Processed {len(deals)} deal(s) for {instrument.symbol} - fees tracked, positions synced from account_all"
495
- )
496
-
497
- def process_order(self, order: Order, update_locked_value: bool = True) -> None:
498
- """
499
- Override process_order to handle Lighter's server-assigned order IDs.
500
-
501
- Lighter assigns server IDs different from our client_id. When an order
502
- update arrives with a new server ID but matching client_id, we need to
503
- migrate the order from client_id key to server_id key while preserving
504
- the same object instance (for external references).
505
-
506
- Args:
507
- order: Order update from WebSocket
508
- update_locked_value: Whether to update locked capital tracking
509
- """
510
- # Check if order exists under client_id (migration case)
511
- if order.client_id and order.client_id in self._active_orders:
512
- # Get the existing order stored under client_id
513
- existing_order = self._active_orders[order.client_id]
514
-
515
- logger.debug(f"Migrating order: client_id={order.client_id} → server_id={order.id}")
516
-
517
- # Remove from old location
518
- self._active_orders.pop(order.client_id)
519
-
520
- # Store it under the new server ID before base class processing
521
- # This allows base class merge logic to find and update it in place
522
- self._active_orders[order.id] = existing_order
523
-
524
- # Also migrate locked capital tracking if present
525
- if order.client_id in self._locked_capital_by_order:
526
- locked_value = self._locked_capital_by_order.pop(order.client_id)
527
- self._locked_capital_by_order[order.id] = locked_value
528
-
529
- # Let base class handle the rest (merge, store, lock/unlock, etc.)
530
- # The base class will now find the existing order under order.id and merge in place
531
- super().process_order(order, update_locked_value)