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.
Files changed (90) hide show
  1. signalflow/__init__.py +21 -0
  2. signalflow/analytics/__init__.py +0 -0
  3. signalflow/core/__init__.py +46 -0
  4. signalflow/core/base_mixin.py +232 -0
  5. signalflow/core/containers/__init__.py +21 -0
  6. signalflow/core/containers/order.py +216 -0
  7. signalflow/core/containers/portfolio.py +211 -0
  8. signalflow/core/containers/position.py +296 -0
  9. signalflow/core/containers/raw_data.py +167 -0
  10. signalflow/core/containers/raw_data_view.py +169 -0
  11. signalflow/core/containers/signals.py +198 -0
  12. signalflow/core/containers/strategy_state.py +147 -0
  13. signalflow/core/containers/trade.py +112 -0
  14. signalflow/core/decorators.py +103 -0
  15. signalflow/core/enums.py +270 -0
  16. signalflow/core/registry.py +322 -0
  17. signalflow/core/rolling_aggregator.py +362 -0
  18. signalflow/core/signal_transforms/__init__.py +5 -0
  19. signalflow/core/signal_transforms/base_signal_transform.py +186 -0
  20. signalflow/data/__init__.py +11 -0
  21. signalflow/data/raw_data_factory.py +225 -0
  22. signalflow/data/raw_store/__init__.py +7 -0
  23. signalflow/data/raw_store/base.py +271 -0
  24. signalflow/data/raw_store/duckdb_stores.py +696 -0
  25. signalflow/data/source/__init__.py +10 -0
  26. signalflow/data/source/base.py +300 -0
  27. signalflow/data/source/binance.py +442 -0
  28. signalflow/data/strategy_store/__init__.py +8 -0
  29. signalflow/data/strategy_store/base.py +278 -0
  30. signalflow/data/strategy_store/duckdb.py +409 -0
  31. signalflow/data/strategy_store/schema.py +36 -0
  32. signalflow/detector/__init__.py +7 -0
  33. signalflow/detector/adapter/__init__.py +5 -0
  34. signalflow/detector/adapter/pandas_detector.py +46 -0
  35. signalflow/detector/base.py +390 -0
  36. signalflow/detector/sma_cross.py +105 -0
  37. signalflow/feature/__init__.py +16 -0
  38. signalflow/feature/adapter/__init__.py +5 -0
  39. signalflow/feature/adapter/pandas_feature_extractor.py +54 -0
  40. signalflow/feature/base.py +330 -0
  41. signalflow/feature/feature_set.py +286 -0
  42. signalflow/feature/oscillator/__init__.py +5 -0
  43. signalflow/feature/oscillator/rsi_extractor.py +42 -0
  44. signalflow/feature/pandasta/__init__.py +10 -0
  45. signalflow/feature/pandasta/pandas_ta_extractor.py +141 -0
  46. signalflow/feature/pandasta/top_pandasta_extractors.py +64 -0
  47. signalflow/feature/smoother/__init__.py +5 -0
  48. signalflow/feature/smoother/sma_extractor.py +46 -0
  49. signalflow/strategy/__init__.py +9 -0
  50. signalflow/strategy/broker/__init__.py +15 -0
  51. signalflow/strategy/broker/backtest.py +172 -0
  52. signalflow/strategy/broker/base.py +186 -0
  53. signalflow/strategy/broker/executor/__init__.py +9 -0
  54. signalflow/strategy/broker/executor/base.py +35 -0
  55. signalflow/strategy/broker/executor/binance_spot.py +12 -0
  56. signalflow/strategy/broker/executor/virtual_spot.py +81 -0
  57. signalflow/strategy/broker/realtime_spot.py +12 -0
  58. signalflow/strategy/component/__init__.py +9 -0
  59. signalflow/strategy/component/base.py +65 -0
  60. signalflow/strategy/component/entry/__init__.py +7 -0
  61. signalflow/strategy/component/entry/fixed_size.py +57 -0
  62. signalflow/strategy/component/entry/signal.py +127 -0
  63. signalflow/strategy/component/exit/__init__.py +5 -0
  64. signalflow/strategy/component/exit/time_based.py +47 -0
  65. signalflow/strategy/component/exit/tp_sl.py +80 -0
  66. signalflow/strategy/component/metric/__init__.py +8 -0
  67. signalflow/strategy/component/metric/main_metrics.py +181 -0
  68. signalflow/strategy/runner/__init__.py +8 -0
  69. signalflow/strategy/runner/backtest_runner.py +208 -0
  70. signalflow/strategy/runner/base.py +19 -0
  71. signalflow/strategy/runner/optimized_backtest_runner.py +178 -0
  72. signalflow/strategy/runner/realtime_runner.py +0 -0
  73. signalflow/target/__init__.py +14 -0
  74. signalflow/target/adapter/__init__.py +5 -0
  75. signalflow/target/adapter/pandas_labeler.py +45 -0
  76. signalflow/target/base.py +409 -0
  77. signalflow/target/fixed_horizon_labeler.py +93 -0
  78. signalflow/target/static_triple_barrier.py +162 -0
  79. signalflow/target/triple_barrier.py +188 -0
  80. signalflow/utils/__init__.py +7 -0
  81. signalflow/utils/import_utils.py +11 -0
  82. signalflow/utils/tune_utils.py +19 -0
  83. signalflow/validator/__init__.py +6 -0
  84. signalflow/validator/base.py +139 -0
  85. signalflow/validator/sklearn_validator.py +527 -0
  86. signalflow_trading-0.2.1.dist-info/METADATA +149 -0
  87. signalflow_trading-0.2.1.dist-info/RECORD +90 -0
  88. signalflow_trading-0.2.1.dist-info/WHEEL +5 -0
  89. signalflow_trading-0.2.1.dist-info/licenses/LICENSE +21 -0
  90. signalflow_trading-0.2.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,188 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, ClassVar
5
+
6
+ import numpy as np
7
+ import polars as pl
8
+ from numba import njit, prange
9
+
10
+ from signalflow.core import sf_component, SignalType
11
+ from signalflow.target.base import Labeler
12
+
13
+
14
+ @njit(parallel=True, cache=True)
15
+ def _find_first_hit(
16
+ prices: np.ndarray,
17
+ pt: np.ndarray,
18
+ sl: np.ndarray,
19
+ lookforward: int,
20
+ ) -> tuple[np.ndarray, np.ndarray]:
21
+ n = len(prices)
22
+ up_off = np.zeros(n, dtype=np.int32)
23
+ dn_off = np.zeros(n, dtype=np.int32)
24
+
25
+ for i in prange(n):
26
+ pt_i = pt[i]
27
+ sl_i = sl[i]
28
+
29
+ if np.isnan(pt_i) or np.isnan(sl_i):
30
+ continue
31
+
32
+ max_j = min(i + lookforward, n - 1)
33
+
34
+ for k in range(1, max_j - i + 1):
35
+ j = i + k
36
+ p = prices[j]
37
+ if up_off[i] == 0 and p >= pt_i:
38
+ up_off[i] = k
39
+ if dn_off[i] == 0 and p <= sl_i:
40
+ dn_off[i] = k
41
+ if up_off[i] > 0 and dn_off[i] > 0:
42
+ break
43
+
44
+ return up_off, dn_off
45
+
46
+
47
+ @dataclass
48
+ @sf_component(name="triple_barrier")
49
+ class TripleBarrierLabeler(Labeler):
50
+ """
51
+ Triple-Barrier Labeling (De Prado), Numba-accelerated.
52
+
53
+ Volatility-based barriers:
54
+ - pt = close * exp(vol * profit_multiplier)
55
+ - sl = close * exp(-vol * stop_loss_multiplier)
56
+ """
57
+
58
+ price_col: str = "close"
59
+
60
+ vol_window: int = 60
61
+ lookforward_window: int = 1440
62
+ profit_multiplier: float = 1.0
63
+ stop_loss_multiplier: float = 1.0
64
+
65
+ def __post_init__(self) -> None:
66
+ if self.vol_window <= 1:
67
+ raise ValueError("vol_window must be > 1")
68
+ if self.lookforward_window <= 0:
69
+ raise ValueError("lookforward_window must be > 0")
70
+ if self.profit_multiplier <= 0 or self.stop_loss_multiplier <= 0:
71
+ raise ValueError("profit_multiplier/stop_loss_multiplier must be > 0")
72
+
73
+ cols = [self.out_col]
74
+ if self.include_meta:
75
+ cols += list(self.meta_columns)
76
+ self.output_columns = cols
77
+
78
+ def compute_group(
79
+ self, group_df: pl.DataFrame, data_context: dict[str, Any] | None
80
+ ) -> pl.DataFrame:
81
+ if self.price_col not in group_df.columns:
82
+ raise ValueError(f"Missing required column '{self.price_col}'")
83
+
84
+ if group_df.height == 0:
85
+ return group_df
86
+
87
+ lf = int(self.lookforward_window)
88
+ vw = int(self.vol_window)
89
+
90
+ df = group_df.with_columns(
91
+ (pl.col(self.price_col) / pl.col(self.price_col).shift(1))
92
+ .log()
93
+ .rolling_std(window_size=vw, ddof=1)
94
+ .alias("_vol")
95
+ ).with_columns(
96
+ [
97
+ (
98
+ pl.col(self.price_col)
99
+ * (pl.col("_vol") * self.profit_multiplier).exp()
100
+ ).alias("_pt"),
101
+ (
102
+ pl.col(self.price_col)
103
+ * (-pl.col("_vol") * self.stop_loss_multiplier).exp()
104
+ ).alias("_sl"),
105
+ ]
106
+ )
107
+
108
+ prices = df.get_column(self.price_col).to_numpy().astype(np.float64)
109
+ pt = df.get_column("_pt").fill_null(np.nan).to_numpy().astype(np.float64)
110
+ sl = df.get_column("_sl").fill_null(np.nan).to_numpy().astype(np.float64)
111
+
112
+ up_off, dn_off = _find_first_hit(prices, pt, sl, lf)
113
+
114
+ up_off_series = pl.Series("_up_off", up_off).replace(0, None).cast(pl.Int32)
115
+ dn_off_series = pl.Series("_dn_off", dn_off).replace(0, None).cast(pl.Int32)
116
+
117
+ df = df.with_columns([up_off_series, dn_off_series])
118
+
119
+ df = self._apply_labels(df)
120
+
121
+ if self.include_meta:
122
+ df = self._compute_meta(df, prices, up_off_series, dn_off_series, lf)
123
+
124
+ if (
125
+ self.mask_to_signals
126
+ and data_context is not None
127
+ and "signal_keys" in data_context
128
+ ):
129
+ df = self._apply_signal_mask(df, data_context, group_df)
130
+
131
+ drop_cols = ["_vol", "_pt", "_sl", "_up_off", "_dn_off"]
132
+ df = df.drop([c for c in drop_cols if c in df.columns])
133
+
134
+ return df
135
+
136
+ def _apply_labels(self, df: pl.DataFrame) -> pl.DataFrame:
137
+ """Apply RISE/FALL/NONE labels based on barrier hits."""
138
+ choose_up = pl.col("_up_off").is_not_null() & (
139
+ pl.col("_dn_off").is_null() | (pl.col("_up_off") <= pl.col("_dn_off"))
140
+ )
141
+ choose_dn = pl.col("_dn_off").is_not_null() & (
142
+ pl.col("_up_off").is_null() | (pl.col("_dn_off") < pl.col("_up_off"))
143
+ )
144
+
145
+ return df.with_columns(
146
+ pl.when(choose_up)
147
+ .then(pl.lit(SignalType.RISE.value))
148
+ .when(choose_dn)
149
+ .then(pl.lit(SignalType.FALL.value))
150
+ .otherwise(pl.lit(SignalType.NONE.value))
151
+ .alias(self.out_col)
152
+ )
153
+
154
+ def _compute_meta(
155
+ self,
156
+ df: pl.DataFrame,
157
+ prices: np.ndarray,
158
+ up_off_series: pl.Series,
159
+ dn_off_series: pl.Series,
160
+ lf: int,
161
+ ) -> pl.DataFrame:
162
+ """Compute t_hit and ret meta columns."""
163
+ n = df.height
164
+ ts_arr = df.get_column(self.ts_col).to_numpy()
165
+
166
+ idx = np.arange(n)
167
+ up_np = up_off_series.fill_null(0).to_numpy()
168
+ dn_np = dn_off_series.fill_null(0).to_numpy()
169
+
170
+ hit_off = np.where(
171
+ (up_np > 0) & ((dn_np == 0) | (up_np <= dn_np)),
172
+ up_np,
173
+ np.where(dn_np > 0, dn_np, 0),
174
+ )
175
+
176
+ hit_idx = np.clip(idx + hit_off, 0, n - 1)
177
+ vert_idx = np.clip(idx + lf, 0, n - 1)
178
+ final_idx = np.where(hit_off > 0, hit_idx, vert_idx)
179
+
180
+ t_hit = ts_arr[final_idx]
181
+ ret = np.where(prices > 0, np.log(prices[final_idx] / prices), np.nan)
182
+
183
+ return df.with_columns(
184
+ [
185
+ pl.Series("t_hit", t_hit),
186
+ pl.Series("ret", ret),
187
+ ]
188
+ )
@@ -0,0 +1,7 @@
1
+ from signalflow.utils.import_utils import import_model_class
2
+ from signalflow.utils.tune_utils import build_optuna_params
3
+
4
+ __all__ = [
5
+ "import_model_class",
6
+ "build_optuna_params",
7
+ ]
@@ -0,0 +1,11 @@
1
+ def import_model_class(class_path: str) -> type:
2
+ """Dynamically import model class from string path."""
3
+ parts = class_path.rsplit(".", 1)
4
+ if len(parts) == 2:
5
+ module_name, class_name = parts
6
+ else:
7
+ raise ValueError(f"Invalid class path: {class_path}")
8
+
9
+ import importlib
10
+ module = importlib.import_module(module_name)
11
+ return getattr(module, class_name)
@@ -0,0 +1,19 @@
1
+ import optuna
2
+ from typing import Any
3
+
4
+
5
+ def build_optuna_params(trial: optuna.Trial, tune_space: dict[str, tuple]) -> dict[str, Any]:
6
+ """Build hyperparameters from optuna trial."""
7
+ params = {}
8
+ for name, spec in tune_space.items():
9
+ param_type = spec[0]
10
+ if param_type == "int":
11
+ params[name] = trial.suggest_int(name, spec[1], spec[2])
12
+ elif param_type == "float":
13
+ params[name] = trial.suggest_float(name, spec[1], spec[2])
14
+ elif param_type == "log_float":
15
+ params[name] = trial.suggest_float(name, spec[1], spec[2], log=True)
16
+ elif param_type == "categorical":
17
+ params[name] = trial.suggest_categorical(name, spec[1])
18
+ return params
19
+
@@ -0,0 +1,6 @@
1
+ from signalflow.validator.base import SignalValidator
2
+ from signalflow.validator.sklearn_validator import SklearnSignalValidator
3
+ __all__ = [
4
+ "SignalValidator",
5
+ "SklearnSignalValidator",
6
+ ]
@@ -0,0 +1,139 @@
1
+ """
2
+ Base Signal Validator for SignalFlow.
3
+
4
+ Signal validators (meta-labelers) predict the quality/risk of trading signals.
5
+ """
6
+
7
+ from dataclasses import dataclass, field
8
+ from typing import Any, ClassVar
9
+ from pathlib import Path
10
+
11
+ import polars as pl
12
+ import numpy as np
13
+
14
+ from signalflow.core import SfComponentType, Signals
15
+
16
+
17
+ @dataclass
18
+ class SignalValidator:
19
+ """Base class for signal validators (meta-labelers).
20
+
21
+ Validates trading signals by predicting their risk/quality.
22
+ In De Prado's terminology - this is a meta-labeler.
23
+
24
+ Note: Filtering to active signals (RISE/FALL only) should be done
25
+ BEFORE passing data to fit. This keeps the validator simple
26
+ and gives users full control over data preparation.
27
+
28
+ Attributes:
29
+ model: The trained model instance
30
+ model_type: String identifier for model type (e.g., "lightgbm", "xgboost")
31
+ model_params: Parameters for model initialization
32
+ train_params: Parameters for training (e.g., early stopping)
33
+ tune_enabled: Whether hyperparameter tuning is enabled
34
+ tune_params: Parameters for tuning (e.g., n_trials, cv_folds)
35
+ feature_columns: List of feature column names (set after fit)
36
+ """
37
+
38
+ component_type: ClassVar[SfComponentType] = SfComponentType.VALIDATOR
39
+
40
+ model: Any | None = None
41
+ model_type: str | None = None
42
+ model_params: dict | None = None
43
+
44
+ train_params: dict | None = None
45
+ tune_enabled: bool = False
46
+ tune_params: dict | None = None
47
+
48
+ feature_columns: list[str] | None = field(default=None, repr=False)
49
+ pair_col: str = "pair"
50
+ ts_col: str = "timestamp"
51
+
52
+ def fit(
53
+ self,
54
+ X_train: pl.DataFrame,
55
+ y_train: pl.DataFrame | pl.Series,
56
+ X_val: pl.DataFrame | None = None,
57
+ y_val: pl.DataFrame | pl.Series | None = None,
58
+ ) -> "SignalValidator":
59
+ """Train the validator model.
60
+
61
+ Args:
62
+ X_train: Training features (Polars DataFrame)
63
+ y_train: Training labels
64
+ X_val: Validation features (optional)
65
+ y_val: Validation labels (optional)
66
+
67
+ Returns:
68
+ Self for method chaining
69
+ """
70
+ raise NotImplementedError("Subclasses must implement fit()")
71
+
72
+ def tune(
73
+ self,
74
+ X_train: pl.DataFrame,
75
+ y_train: pl.DataFrame | pl.Series,
76
+ X_val: pl.DataFrame | None = None,
77
+ y_val: pl.DataFrame | pl.Series | None = None,
78
+ ) -> dict[str, Any]:
79
+ """Tune hyperparameters.
80
+
81
+ Returns:
82
+ Best parameters found
83
+ """
84
+ if not self.tune_enabled:
85
+ raise ValueError("Tuning is not enabled for this validator")
86
+ raise NotImplementedError("Subclasses must implement tune()")
87
+
88
+ def predict(self, signals: Signals, X: pl.DataFrame) -> Signals:
89
+ """Predict class labels and return updated Signals.
90
+
91
+ Args:
92
+ signals: Input signals container
93
+ X: Features (Polars DataFrame) with (pair, timestamp) + feature columns
94
+
95
+ Returns:
96
+ New Signals with prediction column added
97
+ """
98
+ raise NotImplementedError("Subclasses must implement predict()")
99
+
100
+ def predict_proba(self, signals: Signals, X: pl.DataFrame) -> Signals:
101
+ """Predict class probabilities and return updated Signals.
102
+
103
+ Args:
104
+ signals: Input signals container
105
+ X: Features (Polars DataFrame)
106
+
107
+ Returns:
108
+ New Signals with probability columns added
109
+ """
110
+ raise NotImplementedError("Subclasses must implement predict_proba()")
111
+
112
+ def validate_signals(
113
+ self,
114
+ signals: Signals,
115
+ features: pl.DataFrame,
116
+ prefix: str = "probability_",
117
+ ) -> Signals:
118
+ """Add validation predictions to signals.
119
+
120
+ Convenience method - calls predict_proba internally.
121
+
122
+ Args:
123
+ signals: Input signals container
124
+ features: Features DataFrame with (pair, timestamp) + feature columns
125
+ prefix: Prefix for probability columns
126
+
127
+ Returns:
128
+ Signals with added validation columns
129
+ """
130
+ raise NotImplementedError("Subclasses must implement validate_signals()")
131
+
132
+ def save(self, path: str | Path) -> None:
133
+ """Save model to file."""
134
+ raise NotImplementedError("Subclasses must implement save()")
135
+
136
+ @classmethod
137
+ def load(cls, path: str | Path) -> "SignalValidator":
138
+ """Load model from file."""
139
+ raise NotImplementedError("Subclasses must implement load()")