Qubx 0.6.0__tar.gz → 0.6.1__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 (108) hide show
  1. {qubx-0.6.0 → qubx-0.6.1}/PKG-INFO +4 -2
  2. {qubx-0.6.0 → qubx-0.6.1}/pyproject.toml +29 -28
  3. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/__init__.py +1 -4
  4. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/cli/release.py +11 -3
  5. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/core/context.py +5 -0
  6. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/core/interfaces.py +46 -6
  7. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/core/mixins/processing.py +25 -0
  8. qubx-0.6.1/src/qubx/exporters/__init__.py +10 -0
  9. qubx-0.6.1/src/qubx/exporters/formatters/__init__.py +11 -0
  10. qubx-0.6.1/src/qubx/exporters/formatters/base.py +162 -0
  11. qubx-0.6.1/src/qubx/exporters/formatters/incremental.py +16 -0
  12. qubx-0.6.1/src/qubx/exporters/formatters/slack.py +183 -0
  13. qubx-0.6.1/src/qubx/exporters/redis_streams.py +177 -0
  14. qubx-0.6.1/src/qubx/exporters/slack.py +174 -0
  15. {qubx-0.6.0 → qubx-0.6.1}/README.md +0 -0
  16. {qubx-0.6.0 → qubx-0.6.1}/build.py +0 -0
  17. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/_nb_magic.py +0 -0
  18. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/backtester/__init__.py +0 -0
  19. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/backtester/account.py +0 -0
  20. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/backtester/broker.py +0 -0
  21. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/backtester/data.py +0 -0
  22. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/backtester/management.py +0 -0
  23. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/backtester/ome.py +0 -0
  24. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/backtester/optimization.py +0 -0
  25. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/backtester/simulated_data.py +0 -0
  26. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/backtester/simulator.py +0 -0
  27. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/backtester/utils.py +0 -0
  28. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/cli/__init__.py +0 -0
  29. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/cli/commands.py +0 -0
  30. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/cli/deploy.py +0 -0
  31. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/cli/misc.py +0 -0
  32. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/connectors/ccxt/__init__.py +0 -0
  33. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/connectors/ccxt/account.py +0 -0
  34. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/connectors/ccxt/broker.py +0 -0
  35. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/connectors/ccxt/customizations.py +0 -0
  36. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/connectors/ccxt/data.py +0 -0
  37. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/connectors/ccxt/exceptions.py +0 -0
  38. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/connectors/ccxt/factory.py +0 -0
  39. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/connectors/ccxt/utils.py +0 -0
  40. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/core/__init__.py +0 -0
  41. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/core/account.py +0 -0
  42. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/core/basics.py +0 -0
  43. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/core/exceptions.py +0 -0
  44. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/core/helpers.py +0 -0
  45. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/core/loggers.py +0 -0
  46. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/core/lookups.py +0 -0
  47. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/core/metrics.py +0 -0
  48. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/core/mixins/__init__.py +0 -0
  49. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/core/mixins/market.py +0 -0
  50. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/core/mixins/subscription.py +0 -0
  51. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/core/mixins/trading.py +0 -0
  52. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/core/mixins/universe.py +0 -0
  53. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/core/series.pxd +0 -0
  54. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/core/series.pyi +0 -0
  55. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/core/series.pyx +0 -0
  56. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/core/utils.pyi +0 -0
  57. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/core/utils.pyx +0 -0
  58. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/data/__init__.py +0 -0
  59. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/data/helpers.py +0 -0
  60. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/data/readers.py +0 -0
  61. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/data/tardis.py +0 -0
  62. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/gathering/simplest.py +0 -0
  63. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/math/__init__.py +0 -0
  64. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/math/stats.py +0 -0
  65. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/pandaz/__init__.py +0 -0
  66. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/pandaz/ta.py +0 -0
  67. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/pandaz/utils.py +0 -0
  68. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/resources/instruments/symbols-binance.cm.json +0 -0
  69. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/resources/instruments/symbols-binance.json +0 -0
  70. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/resources/instruments/symbols-binance.um.json +0 -0
  71. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/resources/instruments/symbols-bitfinex.f.json +0 -0
  72. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/resources/instruments/symbols-bitfinex.json +0 -0
  73. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/resources/instruments/symbols-kraken.f.json +0 -0
  74. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/resources/instruments/symbols-kraken.json +0 -0
  75. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/ta/__init__.py +0 -0
  76. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/ta/indicators.pxd +0 -0
  77. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/ta/indicators.pyi +0 -0
  78. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/ta/indicators.pyx +0 -0
  79. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/trackers/__init__.py +0 -0
  80. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/trackers/abvanced.py +0 -0
  81. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/trackers/composite.py +0 -0
  82. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/trackers/rebalancers.py +0 -0
  83. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/trackers/riskctrl.py +0 -0
  84. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/trackers/sizers.py +0 -0
  85. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/utils/__init__.py +0 -0
  86. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/utils/_pyxreloader.py +0 -0
  87. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/utils/charting/lookinglass.py +0 -0
  88. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/utils/charting/mpl_helpers.py +0 -0
  89. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/utils/marketdata/binance.py +0 -0
  90. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/utils/marketdata/ccxt.py +0 -0
  91. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/utils/marketdata/dukas.py +0 -0
  92. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/utils/misc.py +0 -0
  93. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/utils/ntp.py +0 -0
  94. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/utils/numbers_utils.py +0 -0
  95. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/utils/orderbook.py +0 -0
  96. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/utils/plotting/__init__.py +0 -0
  97. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/utils/plotting/dashboard.py +0 -0
  98. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/utils/plotting/data.py +0 -0
  99. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/utils/plotting/interfaces.py +0 -0
  100. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/utils/plotting/renderers/__init__.py +0 -0
  101. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/utils/plotting/renderers/plotly.py +0 -0
  102. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/utils/runner/__init__.py +0 -0
  103. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/utils/runner/_jupyter_runner.pyt +0 -0
  104. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/utils/runner/accounts.py +0 -0
  105. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/utils/runner/configs.py +0 -0
  106. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/utils/runner/runner.py +0 -0
  107. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/utils/time.py +0 -0
  108. {qubx-0.6.0 → qubx-0.6.1}/src/qubx/utils/version.py +0 -0
@@ -1,7 +1,8 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.1
2
2
  Name: Qubx
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: Qubx - Quantitative Trading Framework
5
+ Home-page: https://github.com/xLydianSoftware/Qubx
5
6
  Author: Dmitry Marienko
6
7
  Author-email: dmitry.marienko@xlydian.com
7
8
  Requires-Python: >=3.10,<4.0
@@ -37,6 +38,7 @@ Requires-Dist: pymongo (>=4.6.1,<5.0.0)
37
38
  Requires-Dist: python-binance (>=1.0.19,<2.0.0)
38
39
  Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
39
40
  Requires-Dist: pyyaml (>=6.0.2,<7.0.0)
41
+ Requires-Dist: redis (>=5.2.1,<6.0.0)
40
42
  Requires-Dist: scikit-learn (>=1.4.2,<2.0.0)
41
43
  Requires-Dist: scipy (>=1.12.0,<2.0.0)
42
44
  Requires-Dist: sortedcontainers (>=2.4.0,<3.0.0)
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "Qubx"
7
- version = "0.6.0"
7
+ version = "0.6.1"
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"
@@ -24,50 +24,51 @@ format = "wheel"
24
24
  [tool.ruff]
25
25
  line-length = 120
26
26
 
27
+ [tool.poetry.build]
28
+ script = "build.py"
29
+ generate-setup-file = false
30
+
31
+ [tool.poetry.scripts]
32
+ qubx = "qubx.cli.commands:main"
33
+
27
34
  [tool.poetry.dependencies]
28
35
  python = ">=3.10,<4.0"
29
36
  numpy = "^1.26.3"
37
+ pandas = "^2.2.2"
38
+ pyarrow = "^15.0.0"
39
+ scipy = "^1.12.0"
40
+ scikit-learn = "^1.4.2"
41
+ statsmodels = "^0.14.2"
42
+ numba = "^0.59.1"
43
+ sortedcontainers = "^2.4.0"
30
44
  ntplib = "^0.4.0"
45
+ python-binance = "^1.0.19"
46
+ ccxt = "^4.2.68"
47
+ pymongo = "^4.6.1"
48
+ redis = "^5.2.1"
49
+ psycopg = "^3.1.18"
50
+ psycopg-binary = "^3.1.19"
51
+ psycopg-pool = "^3.2.2"
52
+ matplotlib = "^3.8.4"
53
+ plotly = "^5.22.0"
54
+ dash = "^2.18.2"
55
+ dash-bootstrap-components = "^1.6.0"
31
56
  loguru = "^0.7.2"
32
57
  tqdm = "*"
33
58
  importlib-metadata = "*"
34
59
  stackprinter = "^0.2.10"
35
- pymongo = "^4.6.1"
36
60
  pydantic = "^2.9.2"
37
61
  python-dotenv = "^1.0.0"
38
- python-binance = "^1.0.19"
39
- pyarrow = "^15.0.0"
40
- scipy = "^1.12.0"
41
62
  cython = "3.0.8"
42
- ccxt = "^4.2.68"
43
63
  croniter = "^2.0.5"
44
- psycopg = "^3.1.18"
45
- pandas = "^2.2.2"
46
- statsmodels = "^0.14.2"
47
- matplotlib = "^3.8.4"
48
- numba = "^0.59.1"
49
- scikit-learn = "^1.4.2"
50
- plotly = "^5.22.0"
51
- psycopg-binary = "^3.1.19"
52
- psycopg-pool = "^3.2.2"
53
- sortedcontainers = "^2.4.0"
54
64
  msgspec = "^0.18.6"
55
65
  pyyaml = "^6.0.2"
56
- dash = "^2.18.2"
57
- dash-bootstrap-components = "^1.6.0"
58
66
  tabulate = "^0.9.0"
59
- jupyter-console = "^6.6.3"
60
67
  toml = "^0.10.2"
61
68
  gitpython = "^3.1.44"
62
- ipywidgets = "^8.1.5"
63
69
  jupyter = "^1.1.1"
64
-
65
- [tool.poetry.build]
66
- script = "build.py"
67
- generate-setup-file = false
68
-
69
- [tool.poetry.scripts]
70
- qubx = "qubx.cli.commands:main"
70
+ jupyter-console = "^6.6.3"
71
+ ipywidgets = "^8.1.5"
71
72
 
72
73
  [tool.ruff.lint]
73
74
  extend-select = [ "I",]
@@ -77,7 +78,7 @@ ignore = [ "E731", "E722", "E741",]
77
78
  asyncio_mode = "auto"
78
79
  asyncio_default_fixture_loop_scope = "function"
79
80
  pythonpath = [ "src",]
80
- markers = [ "integration: mark a test as an integration test",]
81
+ markers = [ "integration: mark test as requiring external services like Redis", "e2e: mark test as requiring external exchange connections and API credentials",]
81
82
  addopts = "--disable-warnings"
82
83
  filterwarnings = [ "ignore:.*Jupyter is migrating.*:DeprecationWarning",]
83
84
 
@@ -68,10 +68,6 @@ class QubxLogConfig:
68
68
  def setup_logger(level: str | None = None, custom_formatter: Callable | None = None):
69
69
  global logger
70
70
 
71
- # First, remove all existing handlers to prevent resource leaks
72
- # Use a safer approach that doesn't rely on internal attributes
73
- logger.remove()
74
-
75
71
  config = {
76
72
  "handlers": [
77
73
  {"sink": sys.stdout, "format": "{time} - {message}"},
@@ -79,6 +75,7 @@ class QubxLogConfig:
79
75
  "extra": {"user": "someone"},
80
76
  }
81
77
  logger.configure(**config)
78
+ logger.remove(None)
82
79
 
83
80
  level = level or QubxLogConfig.get_log_level()
84
81
  # Add stdout handler with enqueue=True for thread/process safety
@@ -406,6 +406,15 @@ def _create_metadata(stg_name: str, git_info: ReleaseInfo, release_dir: str) ->
406
406
  sort_keys=False,
407
407
  )
408
408
 
409
+ # Create a README.md file
410
+ with open(os.path.join(release_dir, "README.md"), "wt") as fs:
411
+ fs.write(f"# {stg_name}\n\n")
412
+ fs.write("## Git Info\n\n")
413
+ fs.write(f"Tag: {git_info.tag}\n")
414
+ fs.write(f"Date: {git_info.time.isoformat()}\n")
415
+ fs.write(f"Author: {git_info.user}\n")
416
+ fs.write(f"Commit: {git_info.commit}\n")
417
+
409
418
 
410
419
  def _modify_pyproject_toml(pyproject_path: str, package_name: str) -> None:
411
420
  """
@@ -553,7 +562,7 @@ def _get_imports(file_name: str, current_directory: str, what_to_look: list[str]
553
562
  + ".py"
554
563
  )
555
564
  imports.extend(_get_imports(f1, current_directory, what_to_look))
556
- except Exception as e:
565
+ except Exception:
557
566
  pass
558
567
  return imports
559
568
 
@@ -582,11 +591,10 @@ def make_tag_in_repo(repo: Repo, strategy_name_id: str, user: str, tag: str) ->
582
591
  repo.config_writer().set_value("push", "followTags", "true").release()
583
592
 
584
593
  _tn = datetime.now()
585
- ref_an = repo.create_tag(
594
+ _ = repo.create_tag(
586
595
  tag,
587
596
  message=f"Release of '{strategy_name_id}' at {_tn.strftime('%Y-%b-%d %H:%M:%S')} by {user}",
588
597
  )
589
- # Fix: Push the tag reference properly
590
598
  repo.remote("origin").push(f"refs/tags/{tag}")
591
599
  return tag
592
600
 
@@ -31,6 +31,7 @@ from qubx.core.interfaces import (
31
31
  IStrategy,
32
32
  IStrategyContext,
33
33
  ISubscriptionManager,
34
+ ITradeDataExport,
34
35
  ITradingManager,
35
36
  IUniverseManager,
36
37
  PositionsTracker,
@@ -70,6 +71,7 @@ class StrategyContext(IStrategyContext):
70
71
 
71
72
  _thread_data_loop: Thread | None = None # market data loop
72
73
  _is_initialized: bool = False
74
+ _exporter: ITradeDataExport | None = None # Add exporter attribute
73
75
 
74
76
  def __init__(
75
77
  self,
@@ -84,6 +86,7 @@ class StrategyContext(IStrategyContext):
84
86
  config: dict[str, Any] | None = None,
85
87
  position_gathering: IPositionGathering | None = None, # TODO: make position gathering part of the strategy
86
88
  aux_data_provider: DataReader | None = None,
89
+ exporter: ITradeDataExport | None = None, # Add exporter parameter
87
90
  ) -> None:
88
91
  self.account = account
89
92
  self.strategy = self.__instantiate_strategy(strategy, config)
@@ -96,6 +99,7 @@ class StrategyContext(IStrategyContext):
96
99
  self._initial_instruments = instruments
97
100
 
98
101
  self._cache = CachedMarketDataHolder()
102
+ self._exporter = exporter # Store the exporter
99
103
 
100
104
  __position_tracker = self.strategy.tracker(self)
101
105
  if __position_tracker is None:
@@ -149,6 +153,7 @@ class StrategyContext(IStrategyContext):
149
153
  cache=self._cache,
150
154
  scheduler=self._scheduler,
151
155
  is_simulation=self._data_provider.is_simulation,
156
+ exporter=self._exporter, # Pass exporter to processing manager
152
157
  )
153
158
  self.__post_init__()
154
159
 
@@ -39,6 +39,46 @@ from qubx.core.series import OHLCV, Bar, Quote
39
39
  RemovalPolicy = Literal["close", "wait_for_close", "wait_for_change"]
40
40
 
41
41
 
42
+ class ITradeDataExport:
43
+ """Interface for exporting trading data to external systems."""
44
+
45
+ def export_signals(self, time: dt_64, signals: List[Signal], account: "IAccountViewer") -> None:
46
+ """
47
+ Export signals to an external system.
48
+
49
+ Args:
50
+ time: Timestamp when the signals were generated
51
+ signals: List of signals to export
52
+ account: Account viewer to get account information like total capital, leverage, etc.
53
+ """
54
+ pass
55
+
56
+ def export_target_positions(self, time: dt_64, targets: List[TargetPosition], account: "IAccountViewer") -> None:
57
+ """
58
+ Export target positions to an external system.
59
+
60
+ Args:
61
+ time: Timestamp when the target positions were generated
62
+ targets: List of target positions to export
63
+ account: Account viewer to get account information like total capital, leverage, etc.
64
+ """
65
+ pass
66
+
67
+ def export_position_changes(
68
+ self, time: dt_64, instrument: Instrument, price: float, account: "IAccountViewer"
69
+ ) -> None:
70
+ """
71
+ Export position changes to an external system.
72
+
73
+ Args:
74
+ time: Timestamp when the leverage change occurred
75
+ instrument: The instrument for which the leverage changed
76
+ price: Price at which the leverage changed
77
+ account: Account viewer to get account information like total capital, leverage, etc.
78
+ """
79
+ pass
80
+
81
+
42
82
  class IAccountViewer:
43
83
  account_id: str
44
84
 
@@ -579,9 +619,9 @@ class IUniverseManager:
579
619
  instruments: List of instruments in the universe
580
620
  skip_callback: Skip callback to the strategy
581
621
  if_has_position_then: What to do if the instrument has a position
582
- - close (default) - close position immediatelly and remove (unsubscribe) instrument from strategy
583
- - wait_for_close - keep instrument and its position until its closed from strategy (or risk management), then remove instrument from strategy
584
- - wait_for_change - keep instrument and position until strategy would try to change it - then close position and remove instrument
622
+ - "close" (default) - close position immediatelly and remove (unsubscribe) instrument from strategy
623
+ - "wait_for_close" - keep instrument and it's position until it's closed from strategy (or risk management), then remove instrument from strategy
624
+ - "wait_for_change" - keep instrument and position until strategy would try to change it - then close position and remove instrument
585
625
  """
586
626
  ...
587
627
 
@@ -599,9 +639,9 @@ class IUniverseManager:
599
639
  Args:
600
640
  instruments: List of instruments to remove
601
641
  if_has_position_then: What to do if the instrument has a position
602
- - close (default) - close position immediatelly and remove (unsubscribe) instrument from strategy
603
- - wait_for_close - keep instrument and its position until its closed from strategy (or risk management), then remove instrument from strategy
604
- - wait_for_change - keep instrument and position until strategy would try to change it - then close position and remove instrument
642
+ - "close" (default) - close position immediatelly and remove (unsubscribe) instrument from strategy
643
+ - "wait_for_close" - keep instrument and it's position until it's closed from strategy (or risk management), then remove instrument from strategy
644
+ - "wait_for_change" - keep instrument and position until strategy would try to change it - then close position and remove instrument
605
645
  """
606
646
  ...
607
647
 
@@ -28,6 +28,7 @@ from qubx.core.interfaces import (
28
28
  IStrategyContext,
29
29
  ISubscriptionManager,
30
30
  ITimeProvider,
31
+ ITradeDataExport,
31
32
  IUniverseManager,
32
33
  PositionsTracker,
33
34
  )
@@ -50,6 +51,7 @@ class ProcessingManager(IProcessingManager):
50
51
  _cache: CachedMarketDataHolder
51
52
  _scheduler: BasicScheduler
52
53
  _universe_manager: IUniverseManager
54
+ _exporter: ITradeDataExport | None = None
53
55
 
54
56
  _handlers: dict[str, Callable[["ProcessingManager", Instrument, str, Any], TriggerEvent | None]]
55
57
  _strategy_name: str
@@ -78,6 +80,7 @@ class ProcessingManager(IProcessingManager):
78
80
  cache: CachedMarketDataHolder,
79
81
  scheduler: BasicScheduler,
80
82
  is_simulation: bool,
83
+ exporter: ITradeDataExport | None = None,
81
84
  ):
82
85
  self._context = context
83
86
  self._strategy = strategy
@@ -92,6 +95,7 @@ class ProcessingManager(IProcessingManager):
92
95
  self._universe_manager = universe_manager
93
96
  self._cache = cache
94
97
  self._scheduler = scheduler
98
+ self._exporter = exporter
95
99
 
96
100
  self._pool = ThreadPool(2) if not self._is_simulation else None
97
101
  self._handlers = {
@@ -239,7 +243,13 @@ class ProcessingManager(IProcessingManager):
239
243
  # - check if trading is allowed for each target position
240
244
  target_positions = [t for t in target_positions if self._universe_manager.is_trading_allowed(t.instrument)]
241
245
 
246
+ # - log target positions
242
247
  self._logging.save_signals_targets(target_positions)
248
+
249
+ # - export target positions if exporter is available
250
+ if self._exporter is not None:
251
+ self._exporter.export_target_positions(self._time_provider.time(), target_positions, self._account)
252
+
243
253
  return target_positions
244
254
 
245
255
  def __process_signals_from_target_positions(
@@ -270,6 +280,10 @@ class ProcessingManager(IProcessingManager):
270
280
  continue
271
281
  signal.reference_price = q.mid_price()
272
282
 
283
+ # - export signals if exporter is available
284
+ if self._exporter is not None and signals:
285
+ self._exporter.export_signals(self._time_provider.time(), signals, self._account)
286
+
273
287
  return signals
274
288
 
275
289
  def _run_in_thread_pool(self, func: Callable, args=()):
@@ -414,14 +428,25 @@ class ProcessingManager(IProcessingManager):
414
428
  self._account.process_deals(instrument, deals)
415
429
  self._logging.save_deals(instrument, deals)
416
430
 
431
+ # - Process all deals first
417
432
  for d in deals:
418
433
  # - notify position gatherer and tracker
419
434
  self._position_gathering.on_execution_report(self._context, instrument, d)
420
435
  self._position_tracker.on_execution_report(self._context, instrument, d)
436
+
421
437
  logger.debug(
422
438
  f"[<y>{self.__class__.__name__}</y>(<g>{instrument}</g>)] :: executed <r>{d.order_id}</r> | {d.amount} @ {d.price}"
423
439
  )
424
440
 
441
+ if self._exporter is not None and (q := self._market_data.quote(instrument)) is not None:
442
+ # - export position changes if exporter is available
443
+ self._exporter.export_position_changes(
444
+ time=self._time_provider.time(),
445
+ instrument=instrument,
446
+ price=q.mid_price(),
447
+ account=self._account,
448
+ )
449
+
425
450
  # - notify universe manager about position change
426
451
  self._universe_manager.on_alter_position(instrument)
427
452
 
@@ -0,0 +1,10 @@
1
+ """
2
+ This module contains exporters for trading data.
3
+
4
+ Exporters are used to export trading data to external systems.
5
+ """
6
+
7
+ from qubx.exporters.redis_streams import RedisStreamsExporter
8
+ from qubx.exporters.slack import SlackExporter
9
+
10
+ __all__ = ["RedisStreamsExporter", "SlackExporter"]
@@ -0,0 +1,11 @@
1
+ """
2
+ Formatters for exporting trading data.
3
+
4
+ This module provides interfaces and implementations for formatting trading data
5
+ before it is exported to external systems.
6
+ """
7
+
8
+ from qubx.exporters.formatters.base import DefaultFormatter, IExportFormatter
9
+ from qubx.exporters.formatters.slack import SlackMessageFormatter
10
+
11
+ __all__ = ["IExportFormatter", "DefaultFormatter", "SlackMessageFormatter"]
@@ -0,0 +1,162 @@
1
+ """
2
+ Base formatter interfaces and implementations.
3
+
4
+ This module provides the base formatter interface and a default implementation
5
+ for formatting trading data before it is exported to external systems.
6
+ """
7
+
8
+ import json
9
+ from abc import ABC, abstractmethod
10
+ from typing import Any
11
+
12
+ import numpy as np
13
+ import pandas as pd
14
+
15
+ from qubx.core.basics import Instrument, Signal, TargetPosition, dt_64
16
+ from qubx.core.interfaces import IAccountViewer
17
+
18
+
19
+ class IExportFormatter(ABC):
20
+ """
21
+ Interface for formatting trading data before export.
22
+
23
+ Formatters are responsible for converting trading data objects (signals, target positions, etc.)
24
+ into a format suitable for export to external systems.
25
+ """
26
+
27
+ @abstractmethod
28
+ def format_signal(self, time: dt_64, signal: Signal, account: IAccountViewer) -> dict[str, Any]:
29
+ """
30
+ Format a signal for export.
31
+
32
+ Args:
33
+ time: Timestamp when the signal was generated
34
+ signal: The signal to format
35
+ account: Account viewer to get account information like total capital, leverage, etc.
36
+ Returns:
37
+ A dictionary containing the formatted signal data
38
+ """
39
+ pass
40
+
41
+ @abstractmethod
42
+ def format_target_position(self, time: dt_64, target: TargetPosition, account: IAccountViewer) -> dict[str, Any]:
43
+ """
44
+ Format a target position for export.
45
+
46
+ Args:
47
+ time: Timestamp when the target position was generated
48
+ target: The target position to format
49
+ account: Account viewer to get account information like total capital, leverage, etc.
50
+
51
+ Returns:
52
+ A dictionary containing the formatted target position data
53
+ """
54
+ pass
55
+
56
+ @abstractmethod
57
+ def format_position_change(
58
+ self, time: dt_64, instrument: Instrument, price: float, account: IAccountViewer
59
+ ) -> dict[str, Any]:
60
+ """
61
+ Format a leverage change for export.
62
+
63
+ Args:
64
+ time: Timestamp when the leverage change occurred
65
+ instrument: The instrument for which the leverage changed
66
+ price: Price at which the leverage changed
67
+ account: Account viewer to get account information like total capital, leverage, etc.
68
+
69
+ Returns:
70
+ A dictionary containing the formatted leverage change data
71
+ """
72
+ pass
73
+
74
+
75
+ class DefaultFormatter(IExportFormatter):
76
+ """
77
+ Default implementation of the IExportFormatter interface.
78
+
79
+ This formatter creates standardized JSON-serializable dictionaries for each data type.
80
+ """
81
+
82
+ def _format_timestamp(self, timestamp: Any) -> str:
83
+ """
84
+ Format a timestamp for export.
85
+
86
+ Args:
87
+ timestamp: The timestamp to format
88
+
89
+ Returns:
90
+ The formatted timestamp as a string
91
+ """
92
+ if timestamp is not None:
93
+ return pd.Timestamp(timestamp).isoformat()
94
+ else:
95
+ return ""
96
+
97
+ def format_signal(self, time: dt_64, signal: Signal, account: IAccountViewer) -> dict[str, Any]:
98
+ """
99
+ Format a signal for export.
100
+
101
+ Args:
102
+ time: Timestamp when the signal was generated
103
+ signal: The signal to format
104
+ account: Account viewer to get account information like total capital, leverage, etc.
105
+
106
+ Returns:
107
+ A dictionary containing the formatted signal data
108
+ """
109
+ return {
110
+ "timestamp": self._format_timestamp(time),
111
+ "instrument": signal.instrument.symbol,
112
+ "exchange": signal.instrument.exchange,
113
+ "direction": str(signal.signal),
114
+ "strength": str(abs(signal.signal)),
115
+ "reference_price": str(signal.reference_price) if signal.reference_price is not None else "",
116
+ "group": signal.group,
117
+ "metadata": json.dumps(signal.options) if signal.options else "",
118
+ }
119
+
120
+ def format_target_position(self, time: dt_64, target: TargetPosition, account: IAccountViewer) -> dict[str, Any]:
121
+ """
122
+ Format a target position for export.
123
+
124
+ Args:
125
+ time: Timestamp when the target position was generated
126
+ target: The target position to format
127
+ account: Account viewer to get account information like total capital, leverage, etc.
128
+
129
+ Returns:
130
+ A dictionary containing the formatted target position data
131
+ """
132
+ return {
133
+ "timestamp": self._format_timestamp(time),
134
+ "instrument": target.instrument.symbol,
135
+ "exchange": target.instrument.exchange,
136
+ "target_size": str(target.target_position_size),
137
+ "price": str(target.price) if target.price is not None else "",
138
+ "signal_id": str(id(target.signal)) if target.signal else "",
139
+ }
140
+
141
+ def format_position_change(
142
+ self, time: dt_64, instrument: Instrument, price: float, account: IAccountViewer
143
+ ) -> dict[str, Any]:
144
+ """
145
+ Format a position change for export.
146
+
147
+ Args:
148
+ time: Timestamp when the leverage change occurred
149
+ instrument: The instrument for which the leverage changed
150
+ price: Price at which the leverage changed
151
+ account: Account viewer to get account information like total capital, leverage, etc.
152
+
153
+ Returns:
154
+ A dictionary containing the formatted leverage change data
155
+ """
156
+ return {
157
+ "timestamp": self._format_timestamp(time),
158
+ "instrument": instrument.symbol,
159
+ "exchange": instrument.exchange,
160
+ "price": price,
161
+ "target_quantity": account.get_position(instrument).quantity,
162
+ }
@@ -0,0 +1,16 @@
1
+ from typing import Any
2
+
3
+ from qubx.core.basics import Instrument, dt_64
4
+ from qubx.core.interfaces import IAccountViewer
5
+ from qubx.exporters.formatters.base import DefaultFormatter
6
+
7
+
8
+ class IncrementalFormatter(DefaultFormatter):
9
+ """
10
+ Incremental formatter for exporting trading data.
11
+ """
12
+
13
+ def format_position_change(
14
+ self, time: dt_64, instrument: Instrument, price: float, account: IAccountViewer
15
+ ) -> dict[str, Any]:
16
+ return {}