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 +1 -4
- qubx/cli/release.py +11 -3
- qubx/core/context.py +5 -0
- qubx/core/interfaces.py +46 -6
- qubx/core/mixins/processing.py +25 -0
- qubx/core/series.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/core/utils.cpython-312-x86_64-linux-gnu.so +0 -0
- qubx/exporters/__init__.py +10 -0
- qubx/exporters/formatters/__init__.py +11 -0
- qubx/exporters/formatters/base.py +162 -0
- qubx/exporters/formatters/incremental.py +16 -0
- qubx/exporters/formatters/slack.py +183 -0
- qubx/exporters/redis_streams.py +177 -0
- qubx/exporters/slack.py +174 -0
- qubx/ta/indicators.cpython-312-x86_64-linux-gnu.so +0 -0
- {qubx-0.6.0.dist-info → qubx-0.6.1.dist-info}/METADATA +4 -2
- {qubx-0.6.0.dist-info → qubx-0.6.1.dist-info}/RECORD +19 -12
- {qubx-0.6.0.dist-info → qubx-0.6.1.dist-info}/WHEEL +1 -1
- {qubx-0.6.0.dist-info → qubx-0.6.1.dist-info}/entry_points.txt +0 -0
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
|
|
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
|
-
|
|
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
|
-
-
|
|
583
|
-
-
|
|
584
|
-
-
|
|
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
|
-
-
|
|
603
|
-
-
|
|
604
|
-
-
|
|
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
|
|
qubx/core/mixins/processing.py
CHANGED
|
@@ -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
|
|
|
Binary file
|
|
Binary file
|
|
@@ -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}")
|
qubx/exporters/slack.py
ADDED
|
@@ -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}")
|
|
Binary file
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: Qubx
|
|
3
|
-
Version: 0.6.
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
102
|
-
qubx-0.6.
|
|
103
|
-
qubx-0.6.
|
|
104
|
-
qubx-0.6.
|
|
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,,
|
|
File without changes
|