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,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,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,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()")
|