signalflow-trading 0.2.1__py3-none-any.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.
- signalflow/__init__.py +21 -0
- signalflow/analytics/__init__.py +0 -0
- signalflow/core/__init__.py +46 -0
- signalflow/core/base_mixin.py +232 -0
- signalflow/core/containers/__init__.py +21 -0
- signalflow/core/containers/order.py +216 -0
- signalflow/core/containers/portfolio.py +211 -0
- signalflow/core/containers/position.py +296 -0
- signalflow/core/containers/raw_data.py +167 -0
- signalflow/core/containers/raw_data_view.py +169 -0
- signalflow/core/containers/signals.py +198 -0
- signalflow/core/containers/strategy_state.py +147 -0
- signalflow/core/containers/trade.py +112 -0
- signalflow/core/decorators.py +103 -0
- signalflow/core/enums.py +270 -0
- signalflow/core/registry.py +322 -0
- signalflow/core/rolling_aggregator.py +362 -0
- signalflow/core/signal_transforms/__init__.py +5 -0
- signalflow/core/signal_transforms/base_signal_transform.py +186 -0
- signalflow/data/__init__.py +11 -0
- signalflow/data/raw_data_factory.py +225 -0
- signalflow/data/raw_store/__init__.py +7 -0
- signalflow/data/raw_store/base.py +271 -0
- signalflow/data/raw_store/duckdb_stores.py +696 -0
- signalflow/data/source/__init__.py +10 -0
- signalflow/data/source/base.py +300 -0
- signalflow/data/source/binance.py +442 -0
- signalflow/data/strategy_store/__init__.py +8 -0
- signalflow/data/strategy_store/base.py +278 -0
- signalflow/data/strategy_store/duckdb.py +409 -0
- signalflow/data/strategy_store/schema.py +36 -0
- signalflow/detector/__init__.py +7 -0
- signalflow/detector/adapter/__init__.py +5 -0
- signalflow/detector/adapter/pandas_detector.py +46 -0
- signalflow/detector/base.py +390 -0
- signalflow/detector/sma_cross.py +105 -0
- signalflow/feature/__init__.py +16 -0
- signalflow/feature/adapter/__init__.py +5 -0
- signalflow/feature/adapter/pandas_feature_extractor.py +54 -0
- signalflow/feature/base.py +330 -0
- signalflow/feature/feature_set.py +286 -0
- signalflow/feature/oscillator/__init__.py +5 -0
- signalflow/feature/oscillator/rsi_extractor.py +42 -0
- signalflow/feature/pandasta/__init__.py +10 -0
- signalflow/feature/pandasta/pandas_ta_extractor.py +141 -0
- signalflow/feature/pandasta/top_pandasta_extractors.py +64 -0
- signalflow/feature/smoother/__init__.py +5 -0
- signalflow/feature/smoother/sma_extractor.py +46 -0
- signalflow/strategy/__init__.py +9 -0
- signalflow/strategy/broker/__init__.py +15 -0
- signalflow/strategy/broker/backtest.py +172 -0
- signalflow/strategy/broker/base.py +186 -0
- signalflow/strategy/broker/executor/__init__.py +9 -0
- signalflow/strategy/broker/executor/base.py +35 -0
- signalflow/strategy/broker/executor/binance_spot.py +12 -0
- signalflow/strategy/broker/executor/virtual_spot.py +81 -0
- signalflow/strategy/broker/realtime_spot.py +12 -0
- signalflow/strategy/component/__init__.py +9 -0
- signalflow/strategy/component/base.py +65 -0
- signalflow/strategy/component/entry/__init__.py +7 -0
- signalflow/strategy/component/entry/fixed_size.py +57 -0
- signalflow/strategy/component/entry/signal.py +127 -0
- signalflow/strategy/component/exit/__init__.py +5 -0
- signalflow/strategy/component/exit/time_based.py +47 -0
- signalflow/strategy/component/exit/tp_sl.py +80 -0
- signalflow/strategy/component/metric/__init__.py +8 -0
- signalflow/strategy/component/metric/main_metrics.py +181 -0
- signalflow/strategy/runner/__init__.py +8 -0
- signalflow/strategy/runner/backtest_runner.py +208 -0
- signalflow/strategy/runner/base.py +19 -0
- signalflow/strategy/runner/optimized_backtest_runner.py +178 -0
- signalflow/strategy/runner/realtime_runner.py +0 -0
- signalflow/target/__init__.py +14 -0
- signalflow/target/adapter/__init__.py +5 -0
- signalflow/target/adapter/pandas_labeler.py +45 -0
- signalflow/target/base.py +409 -0
- signalflow/target/fixed_horizon_labeler.py +93 -0
- signalflow/target/static_triple_barrier.py +162 -0
- signalflow/target/triple_barrier.py +188 -0
- signalflow/utils/__init__.py +7 -0
- signalflow/utils/import_utils.py +11 -0
- signalflow/utils/tune_utils.py +19 -0
- signalflow/validator/__init__.py +6 -0
- signalflow/validator/base.py +139 -0
- signalflow/validator/sklearn_validator.py +527 -0
- signalflow_trading-0.2.1.dist-info/METADATA +149 -0
- signalflow_trading-0.2.1.dist-info/RECORD +90 -0
- signalflow_trading-0.2.1.dist-info/WHEEL +5 -0
- signalflow_trading-0.2.1.dist-info/licenses/LICENSE +21 -0
- signalflow_trading-0.2.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from typing import Any, Type
|
|
2
|
+
from signalflow.core.registry import default_registry
|
|
3
|
+
from signalflow.core.enums import SfComponentType
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def sf_component(*, name: str, override: bool = False):
|
|
7
|
+
"""Register class as SignalFlow component.
|
|
8
|
+
|
|
9
|
+
Decorator that registers a class in the global component registry,
|
|
10
|
+
making it discoverable by name for dynamic instantiation.
|
|
11
|
+
|
|
12
|
+
The decorated class must have a `component_type` class attribute
|
|
13
|
+
of type `SfComponentType` to indicate what kind of component it is
|
|
14
|
+
(e.g., DETECTOR, EXTRACTOR, LABELER, ENTRY_RULE, EXIT_RULE).
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
name (str): Registry name for the component (case-insensitive).
|
|
18
|
+
override (bool): Allow overriding existing registration. Default: False.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
Callable: Decorator function that registers and returns the class unchanged.
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
ValueError: If class doesn't define component_type attribute.
|
|
25
|
+
ValueError: If name already registered and override=False.
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
```python
|
|
29
|
+
from signalflow.core import sf_component
|
|
30
|
+
from signalflow.core.enums import SfComponentType
|
|
31
|
+
from signalflow.detector import SignalDetector
|
|
32
|
+
|
|
33
|
+
@sf_component(name="my_detector")
|
|
34
|
+
class MyDetector(SignalDetector):
|
|
35
|
+
component_type = SfComponentType.DETECTOR
|
|
36
|
+
|
|
37
|
+
def detect(self, df):
|
|
38
|
+
# Detection logic
|
|
39
|
+
return signals
|
|
40
|
+
|
|
41
|
+
# Later, instantiate by name
|
|
42
|
+
from signalflow.core.registry import default_registry
|
|
43
|
+
|
|
44
|
+
detector_cls = default_registry.get(
|
|
45
|
+
SfComponentType.DETECTOR,
|
|
46
|
+
"my_detector"
|
|
47
|
+
)
|
|
48
|
+
detector = detector_cls(params={"window": 20})
|
|
49
|
+
|
|
50
|
+
# Override existing registration
|
|
51
|
+
@sf_component(name="my_detector", override=True)
|
|
52
|
+
class ImprovedDetector(SignalDetector):
|
|
53
|
+
component_type = SfComponentType.DETECTOR
|
|
54
|
+
# ... improved implementation
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
```python
|
|
59
|
+
# Register multiple component types
|
|
60
|
+
|
|
61
|
+
@sf_component(name="sma_cross")
|
|
62
|
+
class SmaCrossDetector(SignalDetector):
|
|
63
|
+
component_type = SfComponentType.DETECTOR
|
|
64
|
+
# ...
|
|
65
|
+
|
|
66
|
+
@sf_component(name="rsi")
|
|
67
|
+
class RsiExtractor(FeatureExtractor):
|
|
68
|
+
component_type = SfComponentType.EXTRACTOR
|
|
69
|
+
# ...
|
|
70
|
+
|
|
71
|
+
@sf_component(name="fixed_size")
|
|
72
|
+
class FixedSizeEntry(SignalEntryRule):
|
|
73
|
+
component_type = SfComponentType.ENTRY_RULE
|
|
74
|
+
# ...
|
|
75
|
+
|
|
76
|
+
@sf_component(name="take_profit")
|
|
77
|
+
class TakeProfitExit(ExitRule):
|
|
78
|
+
component_type = SfComponentType.EXIT_RULE
|
|
79
|
+
# ...
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Note:
|
|
83
|
+
Component names are case-insensitive for lookup.
|
|
84
|
+
The class itself is not modified - only registered.
|
|
85
|
+
Use override=True carefully to avoid accidental overrides.
|
|
86
|
+
"""
|
|
87
|
+
def decorator(cls: Type[Any]) -> Type[Any]:
|
|
88
|
+
component_type = getattr(cls, "component_type", None)
|
|
89
|
+
if not isinstance(component_type, SfComponentType):
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"{cls.__name__} must define class attribute "
|
|
92
|
+
f"'component_type: SfComponentType'"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
default_registry.register(
|
|
96
|
+
component_type,
|
|
97
|
+
name=name,
|
|
98
|
+
cls=cls,
|
|
99
|
+
override=override,
|
|
100
|
+
)
|
|
101
|
+
return cls
|
|
102
|
+
|
|
103
|
+
return decorator
|
signalflow/core/enums.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SignalType(str, Enum):
|
|
5
|
+
"""Enumeration of signal types.
|
|
6
|
+
|
|
7
|
+
Represents the direction of a trading signal detected by signal detectors.
|
|
8
|
+
|
|
9
|
+
Values:
|
|
10
|
+
NONE: No signal detected or neutral state.
|
|
11
|
+
RISE: Bullish signal indicating potential price increase.
|
|
12
|
+
FALL: Bearish signal indicating potential price decrease.
|
|
13
|
+
|
|
14
|
+
Example:
|
|
15
|
+
```python
|
|
16
|
+
from signalflow.core.enums import SignalType
|
|
17
|
+
|
|
18
|
+
# Check signal type
|
|
19
|
+
if signal_type == SignalType.RISE:
|
|
20
|
+
print("Bullish signal detected")
|
|
21
|
+
elif signal_type == SignalType.FALL:
|
|
22
|
+
print("Bearish signal detected")
|
|
23
|
+
else:
|
|
24
|
+
print("No signal")
|
|
25
|
+
|
|
26
|
+
# Use in DataFrame
|
|
27
|
+
import polars as pl
|
|
28
|
+
signals_df = pl.DataFrame({
|
|
29
|
+
"pair": ["BTCUSDT"],
|
|
30
|
+
"timestamp": [datetime.now()],
|
|
31
|
+
"signal_type": [SignalType.RISE.value]
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
# Compare with enum
|
|
35
|
+
is_rise = signals_df.filter(
|
|
36
|
+
pl.col("signal_type") == SignalType.RISE.value
|
|
37
|
+
)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Note:
|
|
41
|
+
Stored as string values in DataFrames for serialization.
|
|
42
|
+
Use .value to get string representation.
|
|
43
|
+
"""
|
|
44
|
+
NONE = "none"
|
|
45
|
+
RISE = "rise"
|
|
46
|
+
FALL = "fall"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class PositionType(str, Enum):
|
|
50
|
+
"""Enumeration of position types.
|
|
51
|
+
|
|
52
|
+
Represents the direction of a trading position.
|
|
53
|
+
|
|
54
|
+
Values:
|
|
55
|
+
LONG: Long position (profit from price increase).
|
|
56
|
+
SHORT: Short position (profit from price decrease).
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
```python
|
|
60
|
+
from signalflow.core import Position, PositionType
|
|
61
|
+
|
|
62
|
+
# Create long position
|
|
63
|
+
long_position = Position(
|
|
64
|
+
pair="BTCUSDT",
|
|
65
|
+
position_type=PositionType.LONG,
|
|
66
|
+
entry_price=45000.0,
|
|
67
|
+
qty=0.5
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Check position type
|
|
71
|
+
if position.position_type == PositionType.LONG:
|
|
72
|
+
print("Long position")
|
|
73
|
+
assert position.side_sign == 1.0
|
|
74
|
+
else:
|
|
75
|
+
print("Short position")
|
|
76
|
+
assert position.side_sign == -1.0
|
|
77
|
+
|
|
78
|
+
# Store in DataFrame
|
|
79
|
+
positions_df = pl.DataFrame({
|
|
80
|
+
"pair": ["BTCUSDT"],
|
|
81
|
+
"position_type": [PositionType.LONG.value],
|
|
82
|
+
"qty": [0.5]
|
|
83
|
+
})
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Note:
|
|
87
|
+
Currently only LONG positions are fully implemented.
|
|
88
|
+
SHORT positions planned for future versions.
|
|
89
|
+
"""
|
|
90
|
+
LONG = "long"
|
|
91
|
+
SHORT = "short"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class SfComponentType(str, Enum):
|
|
95
|
+
"""Enumeration of SignalFlow component types.
|
|
96
|
+
|
|
97
|
+
Defines all component types that can be registered in the component registry.
|
|
98
|
+
Used by sf_component decorator and SignalFlowRegistry for type-safe registration.
|
|
99
|
+
|
|
100
|
+
Component categories:
|
|
101
|
+
- Data: Raw data loading and storage
|
|
102
|
+
- Feature: Feature extraction
|
|
103
|
+
- Signals: Signal detection, transformation, labeling, validation
|
|
104
|
+
- Strategy: Execution, rules, metrics
|
|
105
|
+
|
|
106
|
+
Values:
|
|
107
|
+
RAW_DATA_STORE: Raw data storage backends (e.g., DuckDB, Parquet).
|
|
108
|
+
RAW_DATA_SOURCE: Raw data sources (e.g., Binance API).
|
|
109
|
+
RAW_DATA_LOADER: Raw data loaders combining source + store.
|
|
110
|
+
FEATURE_EXTRACTOR: Feature extraction classes (e.g., RSI, SMA).
|
|
111
|
+
SIGNALS_TRANSFORM: Signal transformation functions.
|
|
112
|
+
LABELER: Signal labeling strategies (e.g., triple barrier).
|
|
113
|
+
DETECTOR: Signal detection algorithms (e.g., SMA cross).
|
|
114
|
+
VALIDATOR: Signal validation models.
|
|
115
|
+
TORCH_MODULE: PyTorch neural network modules.
|
|
116
|
+
VALIDATOR_MODEL: Pre-trained validator models.
|
|
117
|
+
STRATEGY_STORE: Strategy state persistence backends.
|
|
118
|
+
STRATEGY_RUNNER: Backtest/live runner implementations.
|
|
119
|
+
STRATEGY_BROKER: Order management and position tracking.
|
|
120
|
+
STRATEGY_EXECUTOR: Order execution engines (backtest/live).
|
|
121
|
+
STRATEGY_EXIT_RULE: Position exit rules (e.g., take profit, stop loss).
|
|
122
|
+
STRATEGY_ENTRY_RULE: Position entry rules (e.g., fixed size).
|
|
123
|
+
STRATEGY_METRIC: Strategy performance metrics.
|
|
124
|
+
|
|
125
|
+
Example:
|
|
126
|
+
```python
|
|
127
|
+
from signalflow.core import sf_component
|
|
128
|
+
from signalflow.core.enums import SfComponentType
|
|
129
|
+
from signalflow.detector import SignalDetector
|
|
130
|
+
|
|
131
|
+
# Register detector
|
|
132
|
+
@sf_component(name="my_detector")
|
|
133
|
+
class MyDetector(SignalDetector):
|
|
134
|
+
component_type = SfComponentType.DETECTOR
|
|
135
|
+
# ... implementation
|
|
136
|
+
|
|
137
|
+
# Register extractor
|
|
138
|
+
@sf_component(name="my_feature")
|
|
139
|
+
class MyExtractor(FeatureExtractor):
|
|
140
|
+
component_type = SfComponentType.FEATURE_EXTRACTOR
|
|
141
|
+
# ... implementation
|
|
142
|
+
|
|
143
|
+
# Register exit rule
|
|
144
|
+
@sf_component(name="my_exit")
|
|
145
|
+
class MyExit(ExitRule):
|
|
146
|
+
component_type = SfComponentType.STRATEGY_EXIT_RULE
|
|
147
|
+
# ... implementation
|
|
148
|
+
|
|
149
|
+
# Use in registry
|
|
150
|
+
from signalflow.core.registry import default_registry
|
|
151
|
+
|
|
152
|
+
detector = default_registry.create(
|
|
153
|
+
SfComponentType.DETECTOR,
|
|
154
|
+
"my_detector"
|
|
155
|
+
)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Note:
|
|
159
|
+
All registered components must have component_type class attribute.
|
|
160
|
+
Component types are organized hierarchically (category/subcategory).
|
|
161
|
+
"""
|
|
162
|
+
RAW_DATA_STORE = "data/store"
|
|
163
|
+
RAW_DATA_SOURCE = "data/source"
|
|
164
|
+
RAW_DATA_LOADER = "data/loader"
|
|
165
|
+
|
|
166
|
+
FEATURE_EXTRACTOR = "feature/extractor"
|
|
167
|
+
|
|
168
|
+
SIGNALS_TRANSFORM = "signals/transform"
|
|
169
|
+
LABELER = "signals/labeler"
|
|
170
|
+
DETECTOR = "signals/detector"
|
|
171
|
+
VALIDATOR = "signals/validator"
|
|
172
|
+
TORCH_MODULE = "torch_module"
|
|
173
|
+
VALIDATOR_MODEL = "signals/validator/model"
|
|
174
|
+
|
|
175
|
+
STRATEGY_STORE = "strategy/store"
|
|
176
|
+
STRATEGY_RUNNER = "strategy/runner"
|
|
177
|
+
STRATEGY_BROKER = "strategy/broker"
|
|
178
|
+
STRATEGY_EXECUTOR = "strategy/executor"
|
|
179
|
+
STRATEGY_EXIT_RULE = "strategy/exit"
|
|
180
|
+
STRATEGY_ENTRY_RULE = "strategy/entry"
|
|
181
|
+
STRATEGY_METRIC = "strategy/metric"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class DataFrameType(str, Enum):
|
|
185
|
+
"""Supported DataFrame backends.
|
|
186
|
+
|
|
187
|
+
Specifies which DataFrame library to use for data processing.
|
|
188
|
+
Used by FeatureExtractor and other components to determine input/output format.
|
|
189
|
+
|
|
190
|
+
Values:
|
|
191
|
+
POLARS: Polars DataFrame (faster, modern).
|
|
192
|
+
PANDAS: Pandas DataFrame (legacy compatibility).
|
|
193
|
+
|
|
194
|
+
Example:
|
|
195
|
+
```python
|
|
196
|
+
from signalflow.core.enums import DataFrameType
|
|
197
|
+
from signalflow.feature import FeatureExtractor
|
|
198
|
+
|
|
199
|
+
# Polars-based extractor
|
|
200
|
+
class MyExtractor(FeatureExtractor):
|
|
201
|
+
df_type = DataFrameType.POLARS
|
|
202
|
+
|
|
203
|
+
def extract(self, df: pl.DataFrame) -> pl.DataFrame:
|
|
204
|
+
return df.with_columns(
|
|
205
|
+
pl.col("close").rolling_mean(20).alias("sma_20")
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# Pandas-based extractor
|
|
209
|
+
class LegacyExtractor(FeatureExtractor):
|
|
210
|
+
df_type = DataFrameType.PANDAS
|
|
211
|
+
|
|
212
|
+
def extract(self, df: pd.DataFrame) -> pd.DataFrame:
|
|
213
|
+
df["sma_20"] = df["close"].rolling(20).mean()
|
|
214
|
+
return df
|
|
215
|
+
|
|
216
|
+
# Use in RawDataView
|
|
217
|
+
from signalflow.core import RawDataView
|
|
218
|
+
|
|
219
|
+
view = RawDataView(raw=raw_data)
|
|
220
|
+
|
|
221
|
+
# Get data in required format
|
|
222
|
+
df_polars = view.get_data("spot", DataFrameType.POLARS)
|
|
223
|
+
df_pandas = view.get_data("spot", DataFrameType.PANDAS)
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Note:
|
|
227
|
+
New code should prefer POLARS for better performance.
|
|
228
|
+
PANDAS supported for backward compatibility and legacy libraries.
|
|
229
|
+
"""
|
|
230
|
+
POLARS = "polars"
|
|
231
|
+
PANDAS = "pandas"
|
|
232
|
+
|
|
233
|
+
class RawDataType(str, Enum):
|
|
234
|
+
"""Supported raw data types.
|
|
235
|
+
|
|
236
|
+
Defines types of market data that can be loaded and processed.
|
|
237
|
+
|
|
238
|
+
Values:
|
|
239
|
+
SPOT: Spot trading data (OHLCV).
|
|
240
|
+
|
|
241
|
+
Example:
|
|
242
|
+
```python
|
|
243
|
+
from signalflow.core.enums import RawDataType
|
|
244
|
+
|
|
245
|
+
# Load spot data
|
|
246
|
+
loader = BinanceLoader(
|
|
247
|
+
pairs=["BTCUSDT", "ETHUSDT"],
|
|
248
|
+
data_type=RawDataType.SPOT
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
raw_data = loader.load(
|
|
252
|
+
datetime_start=datetime(2024, 1, 1),
|
|
253
|
+
datetime_end=datetime(2024, 12, 31)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Access spot data
|
|
257
|
+
spot_df = raw_data[RawDataType.SPOT.value]
|
|
258
|
+
|
|
259
|
+
# Check data type
|
|
260
|
+
if raw_data_type == RawDataType.SPOT:
|
|
261
|
+
print("Processing spot data")
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
Note:
|
|
265
|
+
Future versions will add:
|
|
266
|
+
- FUTURES: Futures trading data
|
|
267
|
+
- PERPETUAL: Perpetual swaps data
|
|
268
|
+
- LOB: Limit order book data
|
|
269
|
+
"""
|
|
270
|
+
SPOT = "spot"
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any, Dict, Type
|
|
5
|
+
from loguru import logger
|
|
6
|
+
from .enums import SfComponentType
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class SignalFlowRegistry:
|
|
11
|
+
"""Component registry for dynamic component discovery and instantiation.
|
|
12
|
+
|
|
13
|
+
Provides centralized registration and lookup for SignalFlow components.
|
|
14
|
+
Components are organized by type (DETECTOR, EXTRACTOR, etc.) and
|
|
15
|
+
accessed by case-insensitive names.
|
|
16
|
+
|
|
17
|
+
Registry structure:
|
|
18
|
+
component_type -> name -> class
|
|
19
|
+
|
|
20
|
+
Supported component types:
|
|
21
|
+
- DETECTOR: Signal detection classes
|
|
22
|
+
- EXTRACTOR: Feature extraction classes
|
|
23
|
+
- LABELER: Signal labeling classes
|
|
24
|
+
- ENTRY_RULE: Position entry rules
|
|
25
|
+
- EXIT_RULE: Position exit rules
|
|
26
|
+
- METRIC: Strategy metrics
|
|
27
|
+
- EXECUTOR: Order execution engines
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
_items (dict[SfComponentType, dict[str, Type[Any]]]):
|
|
31
|
+
Internal storage mapping component types to name-class pairs.
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
```python
|
|
35
|
+
from signalflow.core.registry import SignalFlowRegistry
|
|
36
|
+
from signalflow.core.enums import SfComponentType
|
|
37
|
+
|
|
38
|
+
# Create registry
|
|
39
|
+
registry = SignalFlowRegistry()
|
|
40
|
+
|
|
41
|
+
# Register component
|
|
42
|
+
registry.register(
|
|
43
|
+
SfComponentType.DETECTOR,
|
|
44
|
+
name="sma_cross",
|
|
45
|
+
cls=SmaCrossDetector
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
# Get component class
|
|
49
|
+
detector_cls = registry.get(SfComponentType.DETECTOR, "sma_cross")
|
|
50
|
+
|
|
51
|
+
# Instantiate component
|
|
52
|
+
detector = registry.create(
|
|
53
|
+
SfComponentType.DETECTOR,
|
|
54
|
+
"sma_cross",
|
|
55
|
+
fast_window=10,
|
|
56
|
+
slow_window=20
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# List available components
|
|
60
|
+
detectors = registry.list(SfComponentType.DETECTOR)
|
|
61
|
+
print(f"Available detectors: {detectors}")
|
|
62
|
+
|
|
63
|
+
# Full snapshot
|
|
64
|
+
snapshot = registry.snapshot()
|
|
65
|
+
print(snapshot)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Note:
|
|
69
|
+
Component names are stored and looked up in lowercase.
|
|
70
|
+
Use default_registry singleton for application-wide registration.
|
|
71
|
+
|
|
72
|
+
See Also:
|
|
73
|
+
sf_component: Decorator for automatic component registration.
|
|
74
|
+
"""
|
|
75
|
+
#TODO: Registry autodiscover
|
|
76
|
+
|
|
77
|
+
_items: Dict[SfComponentType, Dict[str, Type[Any]]] = field(default_factory=dict)
|
|
78
|
+
|
|
79
|
+
def _ensure(self, component_type: SfComponentType) -> None:
|
|
80
|
+
"""Ensure component type exists in registry.
|
|
81
|
+
|
|
82
|
+
Initializes empty dict for component_type if not present.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
component_type (SfComponentType): Component type to ensure.
|
|
86
|
+
"""
|
|
87
|
+
self._items.setdefault(component_type, {})
|
|
88
|
+
|
|
89
|
+
def register(self, component_type: SfComponentType, name: str, cls: Type[Any], *, override: bool = False) -> None:
|
|
90
|
+
"""Register a class under (component_type, name).
|
|
91
|
+
|
|
92
|
+
Stores class in registry for later lookup and instantiation.
|
|
93
|
+
Names are normalized to lowercase for case-insensitive lookup.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
component_type (SfComponentType): Type of component (DETECTOR, EXTRACTOR, etc.).
|
|
97
|
+
name (str): Registry name (case-insensitive, will be lowercased).
|
|
98
|
+
cls (Type[Any]): Class to register.
|
|
99
|
+
override (bool): Allow overriding existing registration. Default: False.
|
|
100
|
+
|
|
101
|
+
Raises:
|
|
102
|
+
ValueError: If name is empty or already registered (when override=False).
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
```python
|
|
106
|
+
# Register new component
|
|
107
|
+
registry.register(
|
|
108
|
+
SfComponentType.DETECTOR,
|
|
109
|
+
name="my_detector",
|
|
110
|
+
cls=MyDetector
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Override existing component
|
|
114
|
+
registry.register(
|
|
115
|
+
SfComponentType.DETECTOR,
|
|
116
|
+
name="my_detector",
|
|
117
|
+
cls=ImprovedDetector,
|
|
118
|
+
override=True # Logs warning
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Register multiple types
|
|
122
|
+
registry.register(SfComponentType.EXTRACTOR, "rsi", RsiExtractor)
|
|
123
|
+
registry.register(SfComponentType.LABELER, "fixed", FixedHorizonLabeler)
|
|
124
|
+
```
|
|
125
|
+
"""
|
|
126
|
+
if not isinstance(name, str) or not name.strip():
|
|
127
|
+
raise ValueError("name must be a non-empty string")
|
|
128
|
+
|
|
129
|
+
key = name.strip().lower()
|
|
130
|
+
self._ensure(component_type)
|
|
131
|
+
|
|
132
|
+
if key in self._items[component_type] and not override:
|
|
133
|
+
raise ValueError(f"{component_type.value}:{key} already registered")
|
|
134
|
+
|
|
135
|
+
if key in self._items[component_type] and override:
|
|
136
|
+
logger.warning(f"Overriding {component_type.value}:{key} with {cls.__name__}")
|
|
137
|
+
|
|
138
|
+
self._items[component_type][key] = cls
|
|
139
|
+
|
|
140
|
+
def get(self, component_type: SfComponentType, name: str) -> Type[Any]:
|
|
141
|
+
"""Get a registered class by key.
|
|
142
|
+
|
|
143
|
+
Lookup is case-insensitive. Raises helpful error with available
|
|
144
|
+
components if key not found.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
component_type (SfComponentType): Type of component to lookup.
|
|
148
|
+
name (str): Component name (case-insensitive).
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
Type[Any]: Registered class.
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
KeyError: If component not found. Error message includes available components.
|
|
155
|
+
|
|
156
|
+
Example:
|
|
157
|
+
```python
|
|
158
|
+
# Get component class
|
|
159
|
+
detector_cls = registry.get(SfComponentType.DETECTOR, "sma_cross")
|
|
160
|
+
|
|
161
|
+
# Case-insensitive
|
|
162
|
+
detector_cls = registry.get(SfComponentType.DETECTOR, "SMA_Cross")
|
|
163
|
+
|
|
164
|
+
# Instantiate manually
|
|
165
|
+
detector = detector_cls(fast_window=10, slow_window=20)
|
|
166
|
+
|
|
167
|
+
# Handle missing component
|
|
168
|
+
try:
|
|
169
|
+
cls = registry.get(SfComponentType.DETECTOR, "unknown")
|
|
170
|
+
except KeyError as e:
|
|
171
|
+
print(f"Component not found: {e}")
|
|
172
|
+
# Shows: "Component not found: DETECTOR:unknown. Available: [sma_cross, ...]"
|
|
173
|
+
```
|
|
174
|
+
"""
|
|
175
|
+
self._ensure(component_type)
|
|
176
|
+
key = name.lower()
|
|
177
|
+
try:
|
|
178
|
+
return self._items[component_type][key]
|
|
179
|
+
except KeyError as e:
|
|
180
|
+
available = ", ".join(sorted(self._items[component_type]))
|
|
181
|
+
raise KeyError(
|
|
182
|
+
f"Component not found: {component_type.value}:{key}. Available: [{available}]"
|
|
183
|
+
) from e
|
|
184
|
+
|
|
185
|
+
def create(self, component_type: SfComponentType, name: str, **kwargs: Any) -> Any:
|
|
186
|
+
"""Instantiate a component by registry key.
|
|
187
|
+
|
|
188
|
+
Convenient method that combines get() and instantiation.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
component_type (SfComponentType): Type of component to create.
|
|
192
|
+
name (str): Component name (case-insensitive).
|
|
193
|
+
**kwargs: Arguments to pass to component constructor.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Any: Instantiated component.
|
|
197
|
+
|
|
198
|
+
Raises:
|
|
199
|
+
KeyError: If component not found.
|
|
200
|
+
TypeError: If kwargs don't match component constructor.
|
|
201
|
+
|
|
202
|
+
Example:
|
|
203
|
+
```python
|
|
204
|
+
# Create detector with params
|
|
205
|
+
detector = registry.create(
|
|
206
|
+
SfComponentType.DETECTOR,
|
|
207
|
+
"sma_cross",
|
|
208
|
+
fast_window=10,
|
|
209
|
+
slow_window=20
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Create extractor
|
|
213
|
+
extractor = registry.create(
|
|
214
|
+
SfComponentType.EXTRACTOR,
|
|
215
|
+
"rsi",
|
|
216
|
+
window=14
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
# Create with config dict
|
|
220
|
+
config = {"window": 20, "threshold": 0.7}
|
|
221
|
+
labeler = registry.create(
|
|
222
|
+
SfComponentType.LABELER,
|
|
223
|
+
"fixed",
|
|
224
|
+
**config
|
|
225
|
+
)
|
|
226
|
+
```
|
|
227
|
+
"""
|
|
228
|
+
cls = self.get(component_type, name)
|
|
229
|
+
return cls(**kwargs)
|
|
230
|
+
|
|
231
|
+
def list(self, component_type: SfComponentType) -> list[str]:
|
|
232
|
+
"""List registered components for a type.
|
|
233
|
+
|
|
234
|
+
Returns sorted list of component names for given type.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
component_type (SfComponentType): Type of components to list.
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
list[str]: Sorted list of registered component names.
|
|
241
|
+
|
|
242
|
+
Example:
|
|
243
|
+
```python
|
|
244
|
+
# List all detectors
|
|
245
|
+
detectors = registry.list(SfComponentType.DETECTOR)
|
|
246
|
+
print(f"Available detectors: {detectors}")
|
|
247
|
+
# Output: ['ema_cross', 'macd', 'rsi_threshold', 'sma_cross']
|
|
248
|
+
|
|
249
|
+
# Check if component exists
|
|
250
|
+
if "sma_cross" in registry.list(SfComponentType.DETECTOR):
|
|
251
|
+
detector = registry.create(SfComponentType.DETECTOR, "sma_cross")
|
|
252
|
+
|
|
253
|
+
# List all component types
|
|
254
|
+
from signalflow.core.enums import SfComponentType
|
|
255
|
+
for component_type in SfComponentType:
|
|
256
|
+
components = registry.list(component_type)
|
|
257
|
+
print(f"{component_type.value}: {components}")
|
|
258
|
+
```
|
|
259
|
+
"""
|
|
260
|
+
self._ensure(component_type)
|
|
261
|
+
return sorted(self._items[component_type])
|
|
262
|
+
|
|
263
|
+
def snapshot(self) -> dict[str, list[str]]:
|
|
264
|
+
"""Snapshot of registry for debugging.
|
|
265
|
+
|
|
266
|
+
Returns complete registry state organized by component type.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
dict[str, list[str]]: Dictionary mapping component type names
|
|
270
|
+
to sorted lists of registered component names.
|
|
271
|
+
|
|
272
|
+
Example:
|
|
273
|
+
```python
|
|
274
|
+
# Get full registry snapshot
|
|
275
|
+
snapshot = registry.snapshot()
|
|
276
|
+
print(snapshot)
|
|
277
|
+
# Output:
|
|
278
|
+
# {
|
|
279
|
+
# 'DETECTOR': ['ema_cross', 'sma_cross'],
|
|
280
|
+
# 'EXTRACTOR': ['rsi', 'sma'],
|
|
281
|
+
# 'LABELER': ['fixed', 'triple_barrier'],
|
|
282
|
+
# 'ENTRY_RULE': ['fixed_size'],
|
|
283
|
+
# 'EXIT_RULE': ['take_profit', 'time_based']
|
|
284
|
+
# }
|
|
285
|
+
|
|
286
|
+
# Use for debugging
|
|
287
|
+
import json
|
|
288
|
+
print(json.dumps(registry.snapshot(), indent=2))
|
|
289
|
+
|
|
290
|
+
# Check registration status
|
|
291
|
+
snapshot = registry.snapshot()
|
|
292
|
+
if 'DETECTOR' in snapshot and 'sma_cross' in snapshot['DETECTOR']:
|
|
293
|
+
print("SMA detector is registered")
|
|
294
|
+
```
|
|
295
|
+
"""
|
|
296
|
+
return {t.value: sorted(v.keys()) for t, v in self._items.items()}
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
default_registry = SignalFlowRegistry()
|
|
300
|
+
"""Global default registry instance.
|
|
301
|
+
|
|
302
|
+
Use this singleton for application-wide component registration.
|
|
303
|
+
|
|
304
|
+
Example:
|
|
305
|
+
```python
|
|
306
|
+
from signalflow.core.registry import default_registry
|
|
307
|
+
from signalflow.core.enums import SfComponentType
|
|
308
|
+
|
|
309
|
+
# Register to default registry
|
|
310
|
+
default_registry.register(
|
|
311
|
+
SfComponentType.DETECTOR,
|
|
312
|
+
"my_detector",
|
|
313
|
+
MyDetector
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Access from anywhere
|
|
317
|
+
detector = default_registry.create(
|
|
318
|
+
SfComponentType.DETECTOR,
|
|
319
|
+
"my_detector"
|
|
320
|
+
)
|
|
321
|
+
```
|
|
322
|
+
"""
|