Qubx 0.6.0__cp312-cp312-manylinux_2_39_x86_64.whl → 0.6.1__cp312-cp312-manylinux_2_39_x86_64.whl

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.

qubx/__init__.py CHANGED
@@ -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
qubx/cli/release.py CHANGED
@@ -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
 
qubx/core/context.py CHANGED
@@ -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
 
qubx/core/interfaces.py CHANGED
@@ -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 {}
@@ -0,0 +1,183 @@
1
+ """
2
+ Slack message formatter for trading data.
3
+
4
+ This module provides a formatter for converting trading data into Slack message blocks
5
+ suitable for posting to Slack channels.
6
+ """
7
+
8
+ import json
9
+ from typing import Any, Dict, List
10
+
11
+ from qubx.core.basics import Instrument, Signal, TargetPosition, dt_64
12
+ from qubx.core.interfaces import IAccountViewer
13
+ from qubx.exporters.formatters.base import DefaultFormatter, IExportFormatter
14
+
15
+
16
+ class SlackMessageFormatter(IExportFormatter):
17
+ """
18
+ Formatter for converting trading data into Slack message blocks.
19
+
20
+ This formatter creates structured Slack message blocks for signals, target positions,
21
+ and position changes, suitable for posting to Slack channels.
22
+ """
23
+
24
+ def __init__(self, strategy_emoji: str = ":chart_with_upwards_trend:", include_account_info: bool = True):
25
+ """
26
+ Initialize the Slack message formatter.
27
+
28
+ Args:
29
+ strategy_emoji: Emoji to use for the strategy in messages
30
+ include_account_info: Whether to include account information in messages
31
+ """
32
+ self._strategy_emoji = strategy_emoji
33
+ self._include_account_info = include_account_info
34
+ self._default_formatter = DefaultFormatter() # For basic data formatting
35
+
36
+ def _format_timestamp(self, timestamp: Any) -> str:
37
+ """Format timestamp for display in Slack messages."""
38
+ return self._default_formatter._format_timestamp(timestamp)
39
+
40
+ def _create_header_block(self, title: str, timestamp: str) -> Dict[str, Any]:
41
+ """Create a header block for Slack messages."""
42
+ return {
43
+ "type": "header",
44
+ "text": {"type": "plain_text", "text": f"{self._strategy_emoji} {title} - {timestamp}", "emoji": True},
45
+ }
46
+
47
+ def _create_section_block(self, text: str) -> Dict[str, Any]:
48
+ """Create a section block for Slack messages."""
49
+ return {"type": "section", "text": {"type": "mrkdwn", "text": text}}
50
+
51
+ def _create_divider_block(self) -> Dict[str, Any]:
52
+ """Create a divider block for Slack messages."""
53
+ return {"type": "divider"}
54
+
55
+ def _format_account_info(self, account: IAccountViewer) -> str:
56
+ """Format account information for inclusion in Slack messages."""
57
+ if not self._include_account_info:
58
+ return ""
59
+
60
+ total_capital = account.get_total_capital()
61
+ return f"*Account Info:*\nTotal Capital: {total_capital:.2f}"
62
+
63
+ def format_signal(self, time: dt_64, signal: Signal, account: IAccountViewer) -> dict[str, Any]:
64
+ """
65
+ Format a signal as a Slack message.
66
+
67
+ Args:
68
+ time: Timestamp when the signal was generated
69
+ signal: The signal to format
70
+ account: Account viewer to get account information
71
+
72
+ Returns:
73
+ A dictionary containing the formatted Slack message blocks
74
+ """
75
+ timestamp = self._format_timestamp(time)
76
+ direction = "🔴 SELL" if signal.signal < 0 else "🟢 BUY" if signal.signal > 0 else "⚪ NEUTRAL"
77
+ strength = abs(signal.signal)
78
+
79
+ # Basic signal data
80
+ signal_text = (
81
+ f"*Signal:* {direction} (Strength: {strength:.2f})\n"
82
+ f"*Instrument:* {signal.instrument.symbol} ({signal.instrument.exchange})\n"
83
+ )
84
+
85
+ # Add reference price if available
86
+ if signal.reference_price is not None:
87
+ signal_text += f"*Reference Price:* {signal.reference_price}\n"
88
+
89
+ # Add group if available
90
+ if signal.group:
91
+ signal_text += f"*Group:* {signal.group}\n"
92
+
93
+ # Add metadata if available
94
+ if signal.options:
95
+ signal_text += f"*Metadata:* {json.dumps(signal.options)}\n"
96
+
97
+ if signal.comment:
98
+ signal_text += f"*Comment:* {signal.comment}\n"
99
+
100
+ # Create blocks
101
+ blocks = [self._create_header_block("New Signal", timestamp), self._create_section_block(signal_text)]
102
+
103
+ # Add account info if enabled
104
+ if self._include_account_info:
105
+ account_text = self._format_account_info(account)
106
+ if account_text:
107
+ blocks.append(self._create_divider_block())
108
+ blocks.append(self._create_section_block(account_text))
109
+
110
+ return {"blocks": blocks}
111
+
112
+ def format_target_position(self, time: dt_64, target: TargetPosition, account: IAccountViewer) -> dict[str, Any]:
113
+ """
114
+ Format a target position as a Slack message.
115
+
116
+ Args:
117
+ time: Timestamp when the target position was generated
118
+ target: The target position to format
119
+ account: Account viewer to get account information
120
+
121
+ Returns:
122
+ A dictionary containing the formatted Slack message blocks
123
+ """
124
+ timestamp = self._format_timestamp(time)
125
+
126
+ # Basic target position data
127
+ target_text = (
128
+ f"*Instrument:* {target.instrument.symbol} ({target.instrument.exchange})\n"
129
+ f"*Target Size:* {target.target_position_size}\n"
130
+ )
131
+
132
+ # Add price if available
133
+ if target.price is not None:
134
+ target_text += f"*Price:* {target.price}\n"
135
+
136
+ # Create blocks
137
+ blocks = [self._create_header_block("Target Position", timestamp), self._create_section_block(target_text)]
138
+
139
+ # Add account info if enabled
140
+ if self._include_account_info:
141
+ account_text = self._format_account_info(account)
142
+ if account_text:
143
+ blocks.append(self._create_divider_block())
144
+ blocks.append(self._create_section_block(account_text))
145
+
146
+ return {"blocks": blocks}
147
+
148
+ def format_position_change(
149
+ self, time: dt_64, instrument: Instrument, price: float, account: IAccountViewer
150
+ ) -> dict[str, Any]:
151
+ """
152
+ Format a position change as a Slack message.
153
+
154
+ Args:
155
+ time: Timestamp when the position change occurred
156
+ instrument: The instrument for which the position changed
157
+ price: Price at which the position changed
158
+ account: Account viewer to get account information
159
+
160
+ Returns:
161
+ A dictionary containing the formatted Slack message blocks
162
+ """
163
+ timestamp = self._format_timestamp(time)
164
+ position = account.get_position(instrument)
165
+
166
+ # Basic position change data
167
+ position_text = (
168
+ f"*Instrument:* {instrument.symbol} ({instrument.exchange})\n"
169
+ f"*Price:* {price}\n"
170
+ f"*Current Quantity:* {position.quantity}\n"
171
+ )
172
+
173
+ # Create blocks
174
+ blocks = [self._create_header_block("Position Change", timestamp), self._create_section_block(position_text)]
175
+
176
+ # Add account info if enabled
177
+ if self._include_account_info:
178
+ account_text = self._format_account_info(account)
179
+ if account_text:
180
+ blocks.append(self._create_divider_block())
181
+ blocks.append(self._create_section_block(account_text))
182
+
183
+ return {"blocks": blocks}
@@ -0,0 +1,177 @@
1
+ """
2
+ Redis Streams Exporter for trading data.
3
+
4
+ This module provides an implementation of ITradeDataExport that exports trading data to Redis Streams.
5
+ """
6
+
7
+ from typing import Any, Dict, List, Optional, TypeVar, cast
8
+
9
+ import redis
10
+ from redis.typing import EncodableT, FieldT
11
+
12
+ from qubx import logger
13
+ from qubx.core.basics import Instrument, Signal, TargetPosition, dt_64
14
+ from qubx.core.interfaces import IAccountViewer, ITradeDataExport
15
+ from qubx.exporters.formatters import DefaultFormatter, IExportFormatter
16
+
17
+
18
+ class RedisStreamsExporter(ITradeDataExport):
19
+ """
20
+ Exports trading data to Redis Streams.
21
+
22
+ This exporter can be configured to export signals, target positions, and leverage changes.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ redis_url: str,
28
+ strategy_name: str,
29
+ export_signals: bool = True,
30
+ export_targets: bool = True,
31
+ export_position_changes: bool = True,
32
+ signals_stream: Optional[str] = None,
33
+ targets_stream: Optional[str] = None,
34
+ position_changes_stream: Optional[str] = None,
35
+ max_stream_length: int = 1000,
36
+ formatter: Optional[IExportFormatter] = None,
37
+ ):
38
+ """
39
+ Initialize the Redis Streams Exporter.
40
+
41
+ Args:
42
+ redis_url: Redis connection URL (e.g., "redis://localhost:6379/0")
43
+ strategy_name: Name of the strategy (used in stream keys if not provided)
44
+ export_signals: Whether to export signals
45
+ export_targets: Whether to export target positions
46
+ export_position_changes: Whether to export position changes
47
+ signals_stream: Custom stream name for signals (default: "strategy:{strategy_name}:signals")
48
+ targets_stream: Custom stream name for target positions (default: "strategy:{strategy_name}:targets")
49
+ position_changes_stream: Custom stream name for position changes (default: "strategy:{strategy_name}:position_changes")
50
+ max_stream_length: Maximum length of each stream
51
+ formatter: Formatter to use for formatting data (default: DefaultFormatter)
52
+ """
53
+ self._redis = redis.from_url(redis_url)
54
+ self._strategy_name = strategy_name
55
+
56
+ self._export_signals = export_signals
57
+ self._export_targets = export_targets
58
+ self._export_position_changes = export_position_changes
59
+
60
+ self._signals_stream = signals_stream or f"strategy:{strategy_name}:signals"
61
+ self._targets_stream = targets_stream or f"strategy:{strategy_name}:targets"
62
+ self._position_changes_stream = position_changes_stream or f"strategy:{strategy_name}:position_changes"
63
+
64
+ self._max_stream_length = max_stream_length
65
+ self._formatter = formatter or DefaultFormatter()
66
+
67
+ self._instrument_to_previous_leverage = {}
68
+
69
+ logger.info(
70
+ f"[RedisStreamsExporter] Initialized for strategy '{strategy_name}' with "
71
+ f"signals: {export_signals}, targets: {export_targets}, position_changes: {export_position_changes}"
72
+ )
73
+
74
+ def _prepare_for_redis(self, data: Dict[str, Any]) -> Dict[FieldT, EncodableT]:
75
+ """
76
+ Prepare data for Redis by ensuring all values are strings.
77
+
78
+ Args:
79
+ data: Dictionary with string keys and any values
80
+
81
+ Returns:
82
+ Dictionary with keys and values compatible with Redis
83
+ """
84
+ # Convert all values to strings and cast the result to the type expected by Redis
85
+ string_dict = {k: str(v) for k, v in data.items()}
86
+ return cast(Dict[FieldT, EncodableT], string_dict)
87
+
88
+ def export_signals(self, time: dt_64, signals: List[Signal], account: IAccountViewer) -> None:
89
+ """
90
+ Export signals to Redis Stream.
91
+
92
+ Args:
93
+ time: Timestamp when the signals were generated
94
+ signals: List of signals to export
95
+ account: Account viewer to get account information like total capital, leverage, etc.
96
+ """
97
+ if not self._export_signals or not signals:
98
+ return
99
+
100
+ try:
101
+ for signal in signals:
102
+ # Format the signal using the formatter
103
+ data = self._formatter.format_signal(time, signal, account)
104
+
105
+ # Prepare data for Redis
106
+ redis_data = self._prepare_for_redis(data)
107
+
108
+ # Add to Redis stream
109
+ self._redis.xadd(self._signals_stream, redis_data, maxlen=self._max_stream_length, approximate=True)
110
+
111
+ logger.debug(f"[RedisStreamsExporter] Exported {len(signals)} signals to {self._signals_stream}")
112
+ except Exception as e:
113
+ logger.error(f"[RedisStreamsExporter] Failed to export signals: {e}")
114
+
115
+ def export_target_positions(self, time: dt_64, targets: List[TargetPosition], account: IAccountViewer) -> None:
116
+ """
117
+ Export target positions to Redis Stream.
118
+
119
+ Args:
120
+ time: Timestamp when the target positions were generated
121
+ targets: List of target positions to export
122
+ account: Account viewer to get account information like total capital, leverage, etc.
123
+ """
124
+ if not self._export_targets or not targets:
125
+ return
126
+
127
+ try:
128
+ for target in targets:
129
+ # Format the target position using the formatter
130
+ data = self._formatter.format_target_position(time, target, account)
131
+
132
+ # Prepare data for Redis
133
+ redis_data = self._prepare_for_redis(data)
134
+
135
+ # Add to Redis stream
136
+ self._redis.xadd(self._targets_stream, redis_data, maxlen=self._max_stream_length, approximate=True)
137
+
138
+ logger.debug(f"[RedisStreamsExporter] Exported {len(targets)} target positions to {self._targets_stream}")
139
+ except Exception as e:
140
+ logger.error(f"[RedisStreamsExporter] Failed to export target positions: {e}")
141
+
142
+ def export_position_changes(
143
+ self, time: dt_64, instrument: Instrument, price: float, account: IAccountViewer
144
+ ) -> None:
145
+ """
146
+ Export leverage changes to Redis Stream.
147
+
148
+ Args:
149
+ time: Timestamp when the leverage change occurred
150
+ instrument: The instrument for which the leverage changed
151
+ price: Price at which the leverage changed
152
+ account: Account viewer to get account information like total capital, leverage, etc.
153
+ """
154
+ if not self._export_position_changes:
155
+ return
156
+
157
+ previous_leverage = self._instrument_to_previous_leverage.get(instrument)
158
+ new_leverage = account.get_leverage(instrument)
159
+
160
+ try:
161
+ # Format the leverage change using the formatter
162
+ data = self._formatter.format_position_change(time, instrument, price, account)
163
+
164
+ # Prepare data for Redis
165
+ redis_data = self._prepare_for_redis(data)
166
+
167
+ # Add to Redis stream
168
+ self._redis.xadd(
169
+ self._position_changes_stream, redis_data, maxlen=self._max_stream_length, approximate=True
170
+ )
171
+
172
+ logger.debug(
173
+ f"[RedisStreamsExporter] Exported position change for {instrument}: "
174
+ f"{previous_leverage} -> {new_leverage} @ {price}"
175
+ )
176
+ except Exception as e:
177
+ logger.error(f"[RedisStreamsExporter] Failed to export position change: {e}")
@@ -0,0 +1,174 @@
1
+ """
2
+ Slack Exporter for trading data.
3
+
4
+ This module provides an implementation of ITradeDataExport that exports trading data to Slack channels.
5
+ """
6
+
7
+ from typing import Dict, List, Optional
8
+
9
+ import requests
10
+
11
+ from qubx import logger
12
+ from qubx.core.basics import Instrument, Signal, TargetPosition, dt_64
13
+ from qubx.core.interfaces import IAccountViewer, ITradeDataExport
14
+ from qubx.exporters.formatters import IExportFormatter, SlackMessageFormatter
15
+
16
+
17
+ class SlackExporter(ITradeDataExport):
18
+ """
19
+ Exports trading data to Slack channels.
20
+
21
+ This exporter can be configured to export signals, target positions, and position changes
22
+ to different Slack channels using webhook URLs.
23
+ """
24
+
25
+ def __init__(
26
+ self,
27
+ strategy_name: str,
28
+ signals_webhook_url: Optional[str] = None,
29
+ targets_webhook_url: Optional[str] = None,
30
+ position_changes_webhook_url: Optional[str] = None,
31
+ export_signals: bool = True,
32
+ export_targets: bool = True,
33
+ export_position_changes: bool = True,
34
+ formatter: Optional[IExportFormatter] = None,
35
+ strategy_emoji: str = ":chart_with_upwards_trend:",
36
+ include_account_info: bool = True,
37
+ ):
38
+ """
39
+ Initialize the Slack Exporter.
40
+
41
+ Args:
42
+ strategy_name: Name of the strategy (used in message headers)
43
+ signals_webhook_url: Webhook URL for signals channel
44
+ targets_webhook_url: Webhook URL for target positions channel
45
+ position_changes_webhook_url: Webhook URL for position changes channel
46
+ export_signals: Whether to export signals
47
+ export_targets: Whether to export target positions
48
+ export_position_changes: Whether to export position changes
49
+ formatter: Formatter to use for formatting data (default: SlackMessageFormatter)
50
+ strategy_emoji: Emoji to use for the strategy in messages
51
+ include_account_info: Whether to include account information in messages
52
+ """
53
+ self._strategy_name = strategy_name
54
+
55
+ self._signals_webhook_url = signals_webhook_url
56
+ self._targets_webhook_url = targets_webhook_url
57
+ self._position_changes_webhook_url = position_changes_webhook_url
58
+
59
+ self._export_signals = export_signals and signals_webhook_url is not None
60
+ self._export_targets = export_targets and targets_webhook_url is not None
61
+ self._export_position_changes = export_position_changes and position_changes_webhook_url is not None
62
+
63
+ # Use provided formatter or create a new SlackMessageFormatter
64
+ self._formatter = formatter or SlackMessageFormatter(
65
+ strategy_emoji=strategy_emoji, include_account_info=include_account_info
66
+ )
67
+
68
+ logger.info(
69
+ f"[SlackExporter] Initialized for strategy '{strategy_name}' with "
70
+ f"signals: {self._export_signals}, targets: {self._export_targets}, "
71
+ f"position_changes: {self._export_position_changes}"
72
+ )
73
+
74
+ def _post_to_slack(self, webhook_url: str, data: Dict) -> bool:
75
+ """
76
+ Post data to a Slack webhook URL.
77
+
78
+ Args:
79
+ webhook_url: The Slack webhook URL to post to
80
+ data: The data to post
81
+
82
+ Returns:
83
+ bool: True if the post was successful, False otherwise
84
+ """
85
+ try:
86
+ response = requests.post(webhook_url, json=data)
87
+ response.raise_for_status()
88
+ return True
89
+ except requests.RequestException as e:
90
+ logger.error(f"[SlackExporter] Failed to post to Slack: {e}")
91
+ return False
92
+
93
+ def export_signals(self, time: dt_64, signals: List[Signal], account: IAccountViewer) -> None:
94
+ """
95
+ Export signals to Slack.
96
+
97
+ Args:
98
+ time: Timestamp when the signals were generated
99
+ signals: List of signals to export
100
+ account: Account viewer to get account information
101
+ """
102
+ if not self._export_signals or not signals or not self._signals_webhook_url:
103
+ return
104
+
105
+ try:
106
+ for signal in signals:
107
+ # Format the signal using the formatter
108
+ data = self._formatter.format_signal(time, signal, account)
109
+
110
+ # Post to Slack
111
+ success = self._post_to_slack(self._signals_webhook_url, data)
112
+
113
+ if success:
114
+ logger.debug(f"[SlackExporter] Exported signal for {signal.instrument} to Slack")
115
+ else:
116
+ logger.warning(f"[SlackExporter] Failed to export signal for {signal.instrument} to Slack")
117
+ except Exception as e:
118
+ logger.error(f"[SlackExporter] Failed to export signals: {e}")
119
+
120
+ def export_target_positions(self, time: dt_64, targets: List[TargetPosition], account: IAccountViewer) -> None:
121
+ """
122
+ Export target positions to Slack.
123
+
124
+ Args:
125
+ time: Timestamp when the target positions were generated
126
+ targets: List of target positions to export
127
+ account: Account viewer to get account information
128
+ """
129
+ if not self._export_targets or not targets or not self._targets_webhook_url:
130
+ return
131
+
132
+ try:
133
+ for target in targets:
134
+ # Format the target position using the formatter
135
+ data = self._formatter.format_target_position(time, target, account)
136
+
137
+ # Post to Slack
138
+ success = self._post_to_slack(self._targets_webhook_url, data)
139
+
140
+ if success:
141
+ logger.debug(f"[SlackExporter] Exported target position for {target.instrument} to Slack")
142
+ else:
143
+ logger.warning(f"[SlackExporter] Failed to export target position for {target.instrument} to Slack")
144
+ except Exception as e:
145
+ logger.error(f"[SlackExporter] Failed to export target positions: {e}")
146
+
147
+ def export_position_changes(
148
+ self, time: dt_64, instrument: Instrument, price: float, account: IAccountViewer
149
+ ) -> None:
150
+ """
151
+ Export position changes to Slack.
152
+
153
+ Args:
154
+ time: Timestamp when the position change occurred
155
+ instrument: The instrument for which the position changed
156
+ price: Price at which the position changed
157
+ account: Account viewer to get account information
158
+ """
159
+ if not self._export_position_changes or not self._position_changes_webhook_url:
160
+ return
161
+
162
+ try:
163
+ # Format the position change using the formatter
164
+ data = self._formatter.format_position_change(time, instrument, price, account)
165
+
166
+ # Post to Slack
167
+ success = self._post_to_slack(self._position_changes_webhook_url, data)
168
+
169
+ if success:
170
+ logger.debug(f"[SlackExporter] Exported position change for {instrument} to Slack")
171
+ else:
172
+ logger.warning(f"[SlackExporter] Failed to export position change for {instrument} to Slack")
173
+ except Exception as e:
174
+ logger.error(f"[SlackExporter] Failed to export position change: {e}")
@@ -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)
@@ -1,4 +1,4 @@
1
- qubx/__init__.py,sha256=Nd_l2nh8OqSHK3uBNlyBBlu1x-KDM6LIJZ-SfdDojWI,8435
1
+ qubx/__init__.py,sha256=GBvbyDpm2yCMJVmGW66Jo0giLOUsKKldDGcVA_r9Ohc,8294
2
2
  qubx/_nb_magic.py,sha256=kcYn8qNb8O223ZRPpq30_n5e__lD5GSVcd0U_jhfnbM,3019
3
3
  qubx/backtester/__init__.py,sha256=OhXhLmj2x6sp6k16wm5IPATvv-E2qRZVIcvttxqPgcg,176
4
4
  qubx/backtester/account.py,sha256=VBFiUMS3So1wVJCmQ3NtZ6Px1zMyi9hVjiq5Cn7sfm8,5838
@@ -14,7 +14,7 @@ qubx/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  qubx/cli/commands.py,sha256=K5dpcXw87ApKn0U00PaQTmHQ3Imq9qFkYYmUfccIjRY,8035
15
15
  qubx/cli/deploy.py,sha256=71mxBiwKnrG-UNtkwwJsWM38zemc_PIBRpREeww5s6k,7638
16
16
  qubx/cli/misc.py,sha256=FkNG2S15FqBv3I0NnvzQCgOuh1RZCbHiZ1VkO3gSHS0,13343
17
- qubx/cli/release.py,sha256=8sBiTTKG8mmUtyZ6qMOJgcQskJGTufiEZP6T8Hx9Yvg,24193
17
+ qubx/cli/release.py,sha256=R9lZR9oWodbJG8TmNgoIuH5LXntq_c0j-LsNoRsw6RU,24508
18
18
  qubx/connectors/ccxt/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  qubx/connectors/ccxt/account.py,sha256=0-NyqDVEu6vim9rBgO4L9Ek3EKHmq2A82eTKpoOSyFM,21630
20
20
  qubx/connectors/ccxt/broker.py,sha256=I91BRQBbVrZm9YGp6AkW_qHSQv-6Qf0H2Rt53Pmh4rk,4114
@@ -26,30 +26,37 @@ qubx/connectors/ccxt/utils.py,sha256=Hn1gK3BCmwbivVjAmwaBy4zQPqwZl0jWbNwdV5_E0po
26
26
  qubx/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
27
  qubx/core/account.py,sha256=MOrllpuZLyaJ1EwsEJst0GaxcC7Z7XMT-VF7OR-1NMQ,10213
28
28
  qubx/core/basics.py,sha256=7CaER2uM73840CUNe9ej9I6BzSkwgydM-RQwVsL5LZ0,27780
29
- qubx/core/context.py,sha256=nOzB_FFzGwjtYo7o8lwBEnJASr1fkd9B_Cc3SCcy7Kw,15616
29
+ qubx/core/context.py,sha256=c9x-y5VHgsbM_snibBp3-_mVD-oZQiQ5xFu-PrPcRXo,15918
30
30
  qubx/core/exceptions.py,sha256=Jidp6v8rF6bCGB4SDNPt5CMHltkd9tbVkHzOvM29KdU,477
31
31
  qubx/core/helpers.py,sha256=7nhO-CgleU6RTXpSwCdMwb0ZwLCYi5hJWnag8kfFDXo,17701
32
- qubx/core/interfaces.py,sha256=giiZAZpNsj5cUCEJMRv9IY1LKqTkbKS41xYksAI24uE,34369
32
+ qubx/core/interfaces.py,sha256=DdKV6MDHDS5GTNRPBd-HTgj7yQz-OXj95_u75IPqO6w,35817
33
33
  qubx/core/loggers.py,sha256=rB56Sh3aRshrYRy2fdNEbPvB9i7757BiPumyKBC2cfs,17888
34
34
  qubx/core/lookups.py,sha256=1oIF3aowRWWNC7DyHaJ5n-v8b9jBlYyOUaA-S_RnGqU,15770
35
35
  qubx/core/metrics.py,sha256=7b9kza1Fu3aGSf4llUiNTIGT1wsRDOaProK5lGk_a5M,57877
36
36
  qubx/core/mixins/__init__.py,sha256=AMCLvfNuIb1kkQl3bhCj9jIOEl2eKcVPJeyLgrkB-rk,329
37
37
  qubx/core/mixins/market.py,sha256=vcUxGsg9Tv-2Df22dXpbFlZmeHcytFDfeP_mWxXpbzE,3264
38
- qubx/core/mixins/processing.py,sha256=dOERoe2TkNPihuXpb1lhawU78gCIdUukZNWThc-JeJY,18097
38
+ qubx/core/mixins/processing.py,sha256=22k2L37PmcnMz3pqjKjpneKJcp8xFXebcp-izd1IJhQ,19125
39
39
  qubx/core/mixins/subscription.py,sha256=J_SX0CNw2bPy4bhxe0vswvDXY4LCkwXSaj_1PepKRLY,8540
40
40
  qubx/core/mixins/trading.py,sha256=CQQIp1t1LJiFph5CiHQR4k4vxTymjFqrkA0awKYn4Dw,3224
41
41
  qubx/core/mixins/universe.py,sha256=1ya2P3QZrsAVXmMXqq0t6CHGAC5iMGVD2ARUAtSfv04,10062
42
- qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=1rOZY9R-8_Ihb7sCPgRdnTekyh6PUJkidy09Cdok8qA,882248
42
+ qubx/core/series.cpython-312-x86_64-linux-gnu.so,sha256=TRLBTp0VKB8mBA_MUHgmtTDhVXNKN5yxjmloG1vIrN0,882248
43
43
  qubx/core/series.pxd,sha256=EqgYT41FrpVB274mDG3jpLCSqK_ykkL-d-1IH8DE1ik,3301
44
44
  qubx/core/series.pyi,sha256=0cgJjqNTWfWCC392jYqnnScAg1rzuZWC0V5aZbfOflw,3889
45
45
  qubx/core/series.pyx,sha256=4XCRdH3otXsU8EJ-g4_zLQfhqR8TVjtEq_e4oDz5mZ4,33836
46
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=t3br3IiZcSbFF40HZeTEaujgoLmEOpR4PH0Bq5E9zhs,86568
46
+ qubx/core/utils.cpython-312-x86_64-linux-gnu.so,sha256=pdhs2kyB6RL1_FJIAfY7JSQFVC2g05AXQau9ka5ndCk,86568
47
47
  qubx/core/utils.pyi,sha256=DAjyRVPJSxK4Em-9wui2F0yYHfP5tI5DjKavXNOnHa8,276
48
48
  qubx/core/utils.pyx,sha256=k5QHfEFvqhqWfCob89ANiJDKNG8gGbOh-O4CVoneZ8M,1696
49
49
  qubx/data/__init__.py,sha256=CTbEWfMC3eVfD4v6OdhEH4AXGNybrnJJ-mxOM-n2e_M,482
50
50
  qubx/data/helpers.py,sha256=GZIQJk5m1rbCX-_heZmJrMZeTpElPT88vGosUIWDuKI,16660
51
51
  qubx/data/readers.py,sha256=RedH9MyOzrYjS3EXNwM_y4nRKbYJtU0Gs6---RFdrBA,55528
52
52
  qubx/data/tardis.py,sha256=LzKSjCEhAupMYlB46SWUo71zSKhSwh26GnGHHxhb9MQ,3769
53
+ qubx/exporters/__init__.py,sha256=7PjLDeDrDc34HOZyEspTvmQydRDIzuvwBlvGzOziXz0,284
54
+ qubx/exporters/formatters/__init__.py,sha256=dSaYV0OTXyA5e2IFLxXgJZNWH4hTrG47-V__0iasZRM,393
55
+ qubx/exporters/formatters/base.py,sha256=6pNQAwTpk56owxXJ2xh_q7Nu_t60ZXk7z0w7JoHIARE,5690
56
+ qubx/exporters/formatters/incremental.py,sha256=tFXJSWJDjlsd5e1r9ZoB6LsLHfVPHlNBBAUFSZ14aKQ,462
57
+ qubx/exporters/formatters/slack.py,sha256=MPjbEFh7PQufPdkg_Fwiu2tVw5zYJa977tCemoI790Y,7017
58
+ qubx/exporters/redis_streams.py,sha256=kbxCWtTEfTL0qqL2V490OKCjfnVVY4AUM_4jxpPzizI,7364
59
+ qubx/exporters/slack.py,sha256=Ang3rlyhzAolzzgSPvnBm7ZHsv3BcnRfBFGzyAYWtr8,7147
53
60
  qubx/gathering/simplest.py,sha256=2BXyzj0wHohVYT1E4Rqwdf9K_gZPoZ_eW9MSe0uliBo,3959
54
61
  qubx/math/__init__.py,sha256=ltHSQj40sCBm3owcvtoZp34h6ws7pZCFcSZgUkTsUCY,114
55
62
  qubx/math/stats.py,sha256=dLfomw5uBYolurxNPKxcGoL27pTEqiTnjI5cZ_-9-gU,4021
@@ -64,7 +71,7 @@ qubx/resources/instruments/symbols-bitfinex.json,sha256=CpzoVgWzGZRN6RpUNhtJVxa3
64
71
  qubx/resources/instruments/symbols-kraken.f.json,sha256=lwNqml3H7lNUl1h3siySSyE1MRcGfqfhb6BcxLsiKr0,212258
65
72
  qubx/resources/instruments/symbols-kraken.json,sha256=RjUTvkQuuu7V1HfSQREvnA4qqkdkB3-rzykDaQds2rQ,456544
66
73
  qubx/ta/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
67
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=rKQBW2in5_A1G0AwgkpS0asNdNPXoAef-MtyUAPftRo,654440
74
+ qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so,sha256=6dtnzC2d4aaGy1A612whYjNR_KpY7ABqYFhSToiZrcc,654440
68
75
  qubx/ta/indicators.pxd,sha256=eCJ9paOxtxbDFx4U5CUhcgB1jjCQAfVqMF2FnbJ03Lo,4222
69
76
  qubx/ta/indicators.pyi,sha256=NJlvN_774UV1U3_lvaYYbCEikLR8sOUo0TdcUGR5GBM,1940
70
77
  qubx/ta/indicators.pyx,sha256=FVkv5ld04TpZMT3a_kR1MU3IUuWfijzjJnh_lG78JxM,26029
@@ -98,7 +105,7 @@ qubx/utils/runner/configs.py,sha256=nQXU1oqtSSGpGHw4cqk1dVpcojibj7bzjWZbDAHRxNc,
98
105
  qubx/utils/runner/runner.py,sha256=Q8kXM0KTr2bvUuMu86eKaNmeQsyD4_LQKpYNaQra5yk,17923
99
106
  qubx/utils/time.py,sha256=1Cvh077Uqf-XjcE5nWp_T9JzFVT6i39kU7Qz-ssHKIo,9630
100
107
  qubx/utils/version.py,sha256=3MwAel409o-Fj_1iM8m46hswldOozvTywOpEMq0BZSo,5311
101
- qubx-0.6.0.dist-info/METADATA,sha256=kgM_2dCdmfYwHpelUMNnFkxmhQVFfszgEO809fHcrCA,3979
102
- qubx-0.6.0.dist-info/WHEEL,sha256=h1DdjcD2ZFnKGsDLjEycQhNNPJ5l-R8qdFdDSXHrAGY,110
103
- qubx-0.6.0.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
104
- qubx-0.6.0.dist-info/RECORD,,
108
+ qubx-0.6.1.dist-info/METADATA,sha256=yedqZAehlC77cCdlEnc1jtAF8dbx_ZO0EWIjQmCsDOo,4068
109
+ qubx-0.6.1.dist-info/WHEEL,sha256=x1HiyTP_r-PIOu3STHzjukjf5kVLXzgVftSXf5bl8AU,110
110
+ qubx-0.6.1.dist-info/entry_points.txt,sha256=VqilDTe8mVuV9SbR-yVlZJBTjbkHIL2JBgXfQw076HY,47
111
+ qubx-0.6.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.1
2
+ Generator: poetry-core 1.9.1
3
3
  Root-Is-Purelib: false
4
4
  Tag: cp312-cp312-manylinux_2_39_x86_64