From: Jérôme Benoit Date: Fri, 21 Nov 2025 19:12:41 +0000 (+0100) Subject: perf(qav3): switch to reversal swing amplitude for labeling HPO X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=26ae3fe75a182916dbe53a4000bedf240f5c6a59;p=freqai-strategies.git perf(qav3): switch to reversal swing amplitude for labeling HPO Signed-off-by: Jérôme Benoit --- diff --git a/README.md b/README.md index d983bf9..2796bb3 100644 --- a/README.md +++ b/README.md @@ -35,75 +35,75 @@ docker compose up -d --build ### Configuration tunables -| Path | Default | Type / Range | Description | -| ---------------------------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| _Protections_ | | | | -| estimated_trade_duration_candles | 48 | int >= 1 | Heuristic for StoplossGuard tuning. | -| _Leverage_ | | | | -| leverage | proposed_leverage | float [1.0, max_leverage] | Leverage. Fallback to proposed_leverage for the pair. | -| _Exit pricing_ | | | | -| exit_pricing.trade_price_target | `moving_average` | enum {`moving_average`,`interpolation`,`weighted_interpolation`} | Trade NATR computation method. | -| exit_pricing.thresholds_calibration.decline_quantile | 0.90 | float (0,1) | PnL decline quantile threshold. | -| _Reversal confirmation_ | | | | -| reversal_confirmation.lookback_period | 0 | int >= 0 | Prior confirming candles; 0 = none. | -| reversal_confirmation.decay_ratio | 0.5 | float (0,1] | Geometric per-candle relaxation factor. | -| reversal_confirmation.min_natr_ratio_percent | 0.0095 | float [0,1] | Lower bound fraction for volatility adjusted reversal threshold. | -| reversal_confirmation.max_natr_ratio_percent | 0.2 | float [0,1] | Upper bound fraction (>= lower bound) for volatility adjusted reversal threshold. | -| _Regressor model_ | | | | -| freqai.regressor | `xgboost` | enum {`xgboost`,`lightgbm`} | Machine learning regressor algorithm. | -| _Extrema smoothing_ | | | | -| freqai.extrema_smoothing.method | `gaussian` | enum {`gaussian`,`kaiser`,`triang`,`smm`,`sma`} | Extrema smoothing kernel (smm=simple moving median, sma=simple moving average). | -| freqai.extrema_smoothing.window | 5 | int >= 3 | Window size for extrema smoothing. | -| freqai.extrema_smoothing.beta | 8.0 | float > 0 | Kaiser kernel shape parameter. | -| _Extrema weighting_ | | | | -| freqai.extrema_weighting.strategy | `none` | enum {`none`,`threshold`} | Weighting strategy applied before smoothing. | -| freqai.extrema_weighting.normalization | `minmax` | enum {`minmax`,`zscore`,`l1`,`l2`,`robust`,`softmax`,`tanh`,`rank`,`none`} | Normalization method for weights. | -| freqai.extrema_weighting.gamma | 1.0 | float (0,10] | Contrast exponent applied after normalization (>1 emphasizes extremes, 0 0 | Temperature parameter for softmax normalization (lower values sharpen distribution, higher values flatten it). | -| freqai.extrema_weighting.tanh_scale | 1.0 | float > 0 | Scale parameter for tanh normalization. | -| freqai.extrema_weighting.tanh_gain | 1.0 | float > 0 | Gain parameter for tanh normalization. | -| freqai.extrema_weighting.robust_quantiles | [0.25, 0.75] | list[float] where 0 <= q_low < q_high <= 1 | Quantile range for robust normalization. | -| freqai.extrema_weighting.rank_method | `average` | enum {`average`,`min`,`max`,`dense`,`ordinal`} | Ranking method for rank normalization. | -| _Feature parameters_ | | | | -| freqai.feature_parameters.label_period_candles | min/max midpoint | int >= 1 | Zigzag labeling NATR horizon. | -| freqai.feature_parameters.min_label_period_candles | 12 | int >= 1 | Minimum labeling NATR horizon used for reversals labeling HPO. | -| freqai.feature_parameters.max_label_period_candles | 24 | int >= 1 | Maximum labeling NATR horizon used for reversals labeling HPO. | -| freqai.feature_parameters.label_natr_ratio | min/max midpoint | float > 0 | Zigzag labeling NATR ratio. | -| freqai.feature_parameters.min_label_natr_ratio | 9.0 | float > 0 | Minimum labeling NATR ratio used for reversals labeling HPO. | -| freqai.feature_parameters.max_label_natr_ratio | 12.0 | float > 0 | Maximum labeling NATR ratio used for reversals labeling HPO. | -| freqai.feature_parameters.label_frequency_candles | `auto` | int >= 2 \| `auto` | Reversals labeling frequency. `auto` = max(2, 2 \* number of whitelisted pairs). | -| freqai.feature_parameters.label_metric | `euclidean` | string (supported: `euclidean`,`minkowski`,`cityblock`,`chebyshev`,`mahalanobis`,`seuclidean`,`jensenshannon`,`sqeuclidean`,...) | Metric used in distance calculations to ideal point. | -| freqai.feature_parameters.label_weights | [0.5,0.5] | list[float] | Per-objective weights used in distance calculations to ideal point. First objective is the median threshold of Zigzag reversals (reversals quality). Second objective is the number of detected reversals. | -| freqai.feature_parameters.label_p_order | `None` | float | p-order used by Minkowski / power-mean calculations (optional). | -| freqai.feature_parameters.label_medoid_metric | `euclidean` | string | Metric used with `medoid`. | -| freqai.feature_parameters.label_kmeans_metric | `euclidean` | string | Metric used for k-means clustering. | -| freqai.feature_parameters.label_kmeans_selection | `min` | enum {`min`,`medoid`} | Strategy to select trial in the best kmeans cluster. | -| freqai.feature_parameters.label_kmedoids_metric | `euclidean` | string | Metric used for k-medoids clustering. | -| freqai.feature_parameters.label_kmedoids_selection | `min` | enum {`min`,`medoid`} | Strategy to select trial in the best k-medoids cluster. | -| freqai.feature_parameters.label_knn_metric | `minkowski` | string | Distance metric for KNN. | -| freqai.feature_parameters.label_knn_p_order | `None` | float | p-order for KNN Minkowski metric distance. (optional) | -| freqai.feature_parameters.label_knn_n_neighbors | 5 | int >= 1 | Number of neighbors for KNN. | -| _Predictions extrema_ | | | | -| freqai.predictions_extrema.selection_method | `rank` | enum {`values`,`rank`,`partition`} | Extrema selection method. `values` uses reversal values, `rank` uses ranked extrema values, `partition` uses sign-based partitioning. | -| freqai.predictions_extrema.thresholds_smoothing | `mean` | enum {`mean`,`median`,`isodata`,`li`,`minimum`,`otsu`,`triangle`,`yen`,`soft_extremum`} | Thresholding method for prediction thresholds smoothing. | -| freqai.predictions_extrema.thresholds_alpha | 12.0 | float > 0 | Alpha for `soft_extremum`. | -| freqai.predictions_extrema.threshold_outlier | 0.999 | float (0,1) | Quantile threshold for predictions outlier filtering. | -| _Optuna / HPO_ | | | | -| freqai.optuna_hyperopt.enabled | true | bool | Enables HPO. | -| freqai.optuna_hyperopt.sampler | `tpe` | enum {`tpe`,`auto`} | HPO sampler algorithm. `tpe` uses TPESampler with multivariate and group, `auto` uses AutoSampler. | -| freqai.optuna_hyperopt.storage | `file` | enum {`file`,`sqlite`} | HPO storage backend. | -| freqai.optuna_hyperopt.continuous | true | bool | Continuous HPO. | -| freqai.optuna_hyperopt.warm_start | true | bool | Warm start HPO with previous best value(s). | -| freqai.optuna_hyperopt.n_startup_trials | 15 | int >= 0 | HPO startup trials. | -| freqai.optuna_hyperopt.n_trials | 50 | int >= 1 | Maximum HPO trials. | -| freqai.optuna_hyperopt.n_jobs | CPU threads / 4 | int >= 1 | Parallel HPO workers. | -| freqai.optuna_hyperopt.timeout | 7200 | int >= 0 | HPO wall-clock timeout in seconds. | -| freqai.optuna_hyperopt.label_candles_step | 1 | int >= 1 | Step for Zigzag NATR horizon search space. | -| freqai.optuna_hyperopt.train_candles_step | 10 | int >= 1 | Step for training sets size search space. | -| freqai.optuna_hyperopt.space_reduction | false | bool | Enable/disable HPO search space reduction based on previous best parameters. | -| freqai.optuna_hyperopt.expansion_ratio | 0.4 | float [0,1] | HPO search space expansion ratio. | -| freqai.optuna_hyperopt.min_resource | 3 | int >= 1 | Minimum resource per Hyperband pruner rung. | -| freqai.optuna_hyperopt.seed | 1 | int >= 0 | HPO RNG seed. | +| Path | Default | Type / Range | Description | +| ---------------------------------------------------- | ----------------- | -------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| _Protections_ | | | | +| estimated_trade_duration_candles | 48 | int >= 1 | Heuristic for StoplossGuard tuning. | +| _Leverage_ | | | | +| leverage | proposed_leverage | float [1.0, max_leverage] | Leverage. Fallback to proposed_leverage for the pair. | +| _Exit pricing_ | | | | +| exit_pricing.trade_price_target | `moving_average` | enum {`moving_average`,`interpolation`,`weighted_interpolation`} | Trade NATR computation method. | +| exit_pricing.thresholds_calibration.decline_quantile | 0.90 | float (0,1) | PnL decline quantile threshold. | +| _Reversal confirmation_ | | | | +| reversal_confirmation.lookback_period | 0 | int >= 0 | Prior confirming candles; 0 = none. | +| reversal_confirmation.decay_ratio | 0.5 | float (0,1] | Geometric per-candle relaxation factor. | +| reversal_confirmation.min_natr_ratio_percent | 0.0095 | float [0,1] | Lower bound fraction for volatility adjusted reversal threshold. | +| reversal_confirmation.max_natr_ratio_percent | 0.2 | float [0,1] | Upper bound fraction (>= lower bound) for volatility adjusted reversal threshold. | +| _Regressor model_ | | | | +| freqai.regressor | `xgboost` | enum {`xgboost`,`lightgbm`} | Machine learning regressor algorithm. | +| _Extrema smoothing_ | | | | +| freqai.extrema_smoothing.method | `gaussian` | enum {`gaussian`,`kaiser`,`triang`,`smm`,`sma`} | Extrema smoothing kernel (smm=simple moving median, sma=simple moving average). | +| freqai.extrema_smoothing.window | 5 | int >= 3 | Window size for extrema smoothing. | +| freqai.extrema_smoothing.beta | 8.0 | float > 0 | Kaiser kernel shape parameter. | +| _Extrema weighting_ | | | | +| freqai.extrema_weighting.strategy | `none` | enum {`none`,`amplitude`,`amplitude_excess`} | Extrema weighting source: unweighted (`none`), swing amplitude (`amplitude`), or volatility-adjusted swing amplitude (`amplitude_excess`). | +| freqai.extrema_weighting.normalization | `minmax` | enum {`minmax`,`zscore`,`l1`,`l2`,`robust`,`softmax`,`tanh`,`rank`,`none`} | Normalization method for weights. | +| freqai.extrema_weighting.gamma | 1.0 | float (0,10] | Contrast exponent applied after normalization (>1 emphasizes extremes, 0 0 | Temperature parameter for softmax normalization (lower values sharpen distribution, higher values flatten it). | +| freqai.extrema_weighting.tanh_scale | 1.0 | float > 0 | Scale parameter for tanh normalization. | +| freqai.extrema_weighting.tanh_gain | 1.0 | float > 0 | Gain parameter for tanh normalization. | +| freqai.extrema_weighting.robust_quantiles | [0.25, 0.75] | list[float] where 0 <= q_low < q_high <= 1 | Quantile range for robust normalization. | +| freqai.extrema_weighting.rank_method | `average` | enum {`average`,`min`,`max`,`dense`,`ordinal`} | Ranking method for rank normalization. | +| _Feature parameters_ | | | | +| freqai.feature_parameters.label_period_candles | min/max midpoint | int >= 1 | Zigzag labeling NATR horizon. | +| freqai.feature_parameters.min_label_period_candles | 12 | int >= 1 | Minimum labeling NATR horizon used for reversals labeling HPO. | +| freqai.feature_parameters.max_label_period_candles | 24 | int >= 1 | Maximum labeling NATR horizon used for reversals labeling HPO. | +| freqai.feature_parameters.label_natr_ratio | min/max midpoint | float > 0 | Zigzag labeling NATR ratio. | +| freqai.feature_parameters.min_label_natr_ratio | 9.0 | float > 0 | Minimum labeling NATR ratio used for reversals labeling HPO. | +| freqai.feature_parameters.max_label_natr_ratio | 12.0 | float > 0 | Maximum labeling NATR ratio used for reversals labeling HPO. | +| freqai.feature_parameters.label_frequency_candles | `auto` | int >= 2 \| `auto` | Reversals labeling frequency. `auto` = max(2, 2 \* number of whitelisted pairs). | +| freqai.feature_parameters.label_metric | `euclidean` | string (supported: `euclidean`,`minkowski`,`cityblock`,`chebyshev`,`mahalanobis`,`seuclidean`,`jensenshannon`,`sqeuclidean`,...) | Metric used in distance calculations to ideal point. | +| freqai.feature_parameters.label_weights | [0.5,0.5] | list[float] | Per-objective weights used in distance calculations to ideal point. First objective is the median swing amplitude of Zigzag reversals (reversals quality). Second objective is the number of detected reversals. | +| freqai.feature_parameters.label_p_order | `None` | float | p-order used by Minkowski / power-mean calculations (optional). | +| freqai.feature_parameters.label_medoid_metric | `euclidean` | string | Metric used with `medoid`. | +| freqai.feature_parameters.label_kmeans_metric | `euclidean` | string | Metric used for k-means clustering. | +| freqai.feature_parameters.label_kmeans_selection | `min` | enum {`min`,`medoid`} | Strategy to select trial in the best kmeans cluster. | +| freqai.feature_parameters.label_kmedoids_metric | `euclidean` | string | Metric used for k-medoids clustering. | +| freqai.feature_parameters.label_kmedoids_selection | `min` | enum {`min`,`medoid`} | Strategy to select trial in the best k-medoids cluster. | +| freqai.feature_parameters.label_knn_metric | `minkowski` | string | Distance metric for KNN. | +| freqai.feature_parameters.label_knn_p_order | `None` | float | p-order for KNN Minkowski metric distance. (optional) | +| freqai.feature_parameters.label_knn_n_neighbors | 5 | int >= 1 | Number of neighbors for KNN. | +| _Predictions extrema_ | | | | +| freqai.predictions_extrema.selection_method | `rank` | enum {`values`,`rank`,`partition`} | Extrema selection method. `values` uses reversal values, `rank` uses ranked extrema values, `partition` uses sign-based partitioning. | +| freqai.predictions_extrema.thresholds_smoothing | `mean` | enum {`mean`,`median`,`isodata`,`li`,`minimum`,`otsu`,`triangle`,`yen`,`soft_extremum`} | Thresholding method for prediction thresholds smoothing. | +| freqai.predictions_extrema.thresholds_alpha | 12.0 | float > 0 | Alpha for `soft_extremum`. | +| freqai.predictions_extrema.threshold_outlier | 0.999 | float (0,1) | Quantile threshold for predictions outlier filtering. | +| _Optuna / HPO_ | | | | +| freqai.optuna_hyperopt.enabled | true | bool | Enables HPO. | +| freqai.optuna_hyperopt.sampler | `tpe` | enum {`tpe`,`auto`} | HPO sampler algorithm. `tpe` uses TPESampler with multivariate and group, `auto` uses AutoSampler. | +| freqai.optuna_hyperopt.storage | `file` | enum {`file`,`sqlite`} | HPO storage backend. | +| freqai.optuna_hyperopt.continuous | true | bool | Continuous HPO. | +| freqai.optuna_hyperopt.warm_start | true | bool | Warm start HPO with previous best value(s). | +| freqai.optuna_hyperopt.n_startup_trials | 15 | int >= 0 | HPO startup trials. | +| freqai.optuna_hyperopt.n_trials | 50 | int >= 1 | Maximum HPO trials. | +| freqai.optuna_hyperopt.n_jobs | CPU threads / 4 | int >= 1 | Parallel HPO workers. | +| freqai.optuna_hyperopt.timeout | 7200 | int >= 0 | HPO wall-clock timeout in seconds. | +| freqai.optuna_hyperopt.label_candles_step | 1 | int >= 1 | Step for Zigzag NATR horizon search space. | +| freqai.optuna_hyperopt.train_candles_step | 10 | int >= 1 | Step for training sets size search space. | +| freqai.optuna_hyperopt.space_reduction | false | bool | Enable/disable HPO search space reduction based on previous best parameters. | +| freqai.optuna_hyperopt.expansion_ratio | 0.4 | float [0,1] | HPO search space expansion ratio. | +| freqai.optuna_hyperopt.min_resource | 3 | int >= 1 | Minimum resource per Hyperband pruner rung. | +| freqai.optuna_hyperopt.seed | 1 | int >= 0 | HPO RNG seed. | ## ReforceXY diff --git a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py index a1e1e6e..2768d3c 100644 --- a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py +++ b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py @@ -21,6 +21,8 @@ from numpy.typing import NDArray from sklearn_extra.cluster import KMedoids from Utils import ( + REGRESSORS, + Regressor, calculate_min_extrema, calculate_n_extrema, fit_regressor, @@ -71,7 +73,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): https://github.com/sponsors/robcaulk """ - version = "3.7.122" + version = "3.7.123" _SQRT_2: Final[float] = np.sqrt(2.0) @@ -300,8 +302,12 @@ class QuickAdapterRegressorV3(BaseRegressionModel): self.set_optuna_label_candle(pair) self._optuna_label_candles[pair] = 0 + self.regressor: Regressor = self.freqai_info.get("regressor", REGRESSORS[0]) + if self.regressor not in REGRESSORS: + self.regressor = REGRESSORS[0] + self.freqai_info["regressor"] = self.regressor logger.info( - f"Initialized {self.__class__.__name__} {self.freqai_info.get('regressor', 'xgboost')} regressor model version {self.version}" + f"Initialized {self.__class__.__name__} {self.regressor} regressor model version {self.version}" ) def get_optuna_params(self, pair: str, namespace: str) -> dict[str, Any]: @@ -451,7 +457,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): namespace=QuickAdapterRegressorV3._OPTUNA_NAMESPACES[0], # "hp" objective=lambda trial: hp_objective( trial, - str(self.freqai_info.get("regressor", "xgboost")), + self.regressor, X, y, train_weights, @@ -482,7 +488,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): namespace=QuickAdapterRegressorV3._OPTUNA_NAMESPACES[1], # "train" objective=lambda trial: train_objective( trial, - str(self.freqai_info.get("regressor", "xgboost")), + self.regressor, X, y, train_weights, @@ -537,7 +543,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): ) model = fit_regressor( - regressor=str(self.freqai_info.get("regressor", "xgboost")), + regressor=self.regressor, X=X, y=y, train_weights=train_weights, @@ -1912,7 +1918,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): def train_objective( trial: optuna.trial.Trial, - regressor: str, + regressor: Regressor, X: pd.DataFrame, y: pd.DataFrame, train_weights: NDArray[np.floating], @@ -2023,7 +2029,7 @@ def train_objective( def hp_objective( trial: optuna.trial.Trial, - regressor: str, + regressor: Regressor, X: pd.DataFrame, y: pd.DataFrame, train_weights: NDArray[np.floating], @@ -2099,10 +2105,13 @@ def label_objective( if df.empty: return -np.inf, 0 - _, pivots_values, _, pivots_thresholds = zigzag( + _, pivots_values, _, pivots_amplitudes, _ = zigzag( df, natr_period=label_period_candles, natr_ratio=label_natr_ratio, ) - return np.median(pivots_thresholds), len(pivots_values) + median_amplitude = np.nanmedian(pivots_amplitudes) + if not np.isfinite(median_amplitude): + median_amplitude = 0.0 + return median_amplitude, len(pivots_values) diff --git a/quickadapter/user_data/strategies/QuickAdapterV3.py b/quickadapter/user_data/strategies/QuickAdapterV3.py index 7221548..e2764ec 100644 --- a/quickadapter/user_data/strategies/QuickAdapterV3.py +++ b/quickadapter/user_data/strategies/QuickAdapterV3.py @@ -104,7 +104,7 @@ class QuickAdapterV3(IStrategy): _TRADING_MODES: Final[tuple[TradingMode, ...]] = ("spot", "margin", "futures") def version(self) -> str: - return "3.3.172" + return "3.3.173" timeframe = "5m" @@ -715,7 +715,7 @@ class QuickAdapterV3(IStrategy): @staticmethod @lru_cache(maxsize=128) - def td_format( + def _td_format( delta: datetime.timedelta, pattern: str = "{sign}{d}:{h:02d}:{m:02d}:{s:02d}" ) -> str: negative_duration = delta.total_seconds() < 0 @@ -730,13 +730,36 @@ class QuickAdapterV3(IStrategy): except (KeyError, ValueError) as e: raise ValueError(f"Invalid pattern '{pattern}': {repr(e)}") + @staticmethod + def _get_weights( + strategy: str, amplitudes: list[float], amplitude_excesses: list[float] + ) -> list[float]: + if not isinstance(strategy, str): + return [] + strategy = strategy.lower().strip() + if strategy == "amplitude_excess": + return ( + amplitude_excesses + if len(amplitude_excesses) == len(amplitudes) + else amplitudes + ) + if strategy == "amplitude": + return amplitudes + return [] + def set_freqai_targets( self, dataframe: DataFrame, metadata: dict[str, Any], **kwargs ) -> DataFrame: pair = str(metadata.get("pair")) label_period_candles = self.get_label_period_candles(pair) label_natr_ratio = self.get_label_natr_ratio(pair) - pivots_indices, _, pivots_directions, pivots_thresholds = zigzag( + ( + pivots_indices, + _, + pivots_directions, + pivots_amplitudes, + pivots_amplitude_excesses, + ) = zigzag( dataframe, natr_period=label_period_candles, natr_ratio=label_natr_ratio, @@ -747,7 +770,7 @@ class QuickAdapterV3(IStrategy): dataframe[EXTREMA_COLUMN] = 0 if len(pivots_indices) == 0: logger.warning( - f"{pair}: no extrema to label (label_period={QuickAdapterV3.td_format(label_period)} / {label_period_candles=} / {label_natr_ratio=:.2f})" + f"{pair}: no extrema to label (label_period={QuickAdapterV3._td_format(label_period)} / {label_period_candles=} / {label_natr_ratio=:.2f})" ) else: dataframe.loc[pivots_indices, EXTREMA_COLUMN] = pivots_directions @@ -758,7 +781,7 @@ class QuickAdapterV3(IStrategy): dataframe[EXTREMA_COLUMN] == TrendDirection.UP, 1, 0 ) logger.info( - f"{pair}: labeled {len(pivots_indices)} extrema (label_period={QuickAdapterV3.td_format(label_period)} / {label_period_candles=} / {label_natr_ratio=:.2f})" + f"{pair}: labeled {len(pivots_indices)} extrema (label_period={QuickAdapterV3._td_format(label_period)} / {label_period_candles=} / {label_natr_ratio=:.2f})" ) extrema_smoothing = self.freqai_info.get("extrema_smoothing", {}) @@ -787,10 +810,15 @@ class QuickAdapterV3(IStrategy): extrema_weighting, pair ) + pivot_weights = QuickAdapterV3._get_weights( + extrema_weighting_params["strategy"], + pivots_amplitudes, + pivots_amplitude_excesses, + ) weighted_extrema, _ = get_weighted_extrema( extrema=dataframe[EXTREMA_COLUMN], indices=pivots_indices, - weights=np.array(pivots_thresholds), + weights=np.array(pivot_weights), strategy=extrema_weighting_params["strategy"], normalization=extrema_weighting_params["normalization"], gamma=extrema_weighting_params["gamma"], diff --git a/quickadapter/user_data/strategies/Utils.py b/quickadapter/user_data/strategies/Utils.py index f19eb51..cdd83f2 100644 --- a/quickadapter/user_data/strategies/Utils.py +++ b/quickadapter/user_data/strategies/Utils.py @@ -18,8 +18,12 @@ from technical import qtpylib T = TypeVar("T", pd.Series, float) -WeightStrategy = Literal["none", "threshold"] -WEIGHT_STRATEGIES: Final[tuple[WeightStrategy, ...]] = ("none", "threshold") +WeightStrategy = Literal["none", "amplitude", "amplitude_excess"] +WEIGHT_STRATEGIES: Final[tuple[WeightStrategy, ...]] = ( + "none", + "amplitude", + "amplitude_excess", +) NormalizationType = Literal[ "minmax", "zscore", "l1", "l2", "robust", "softmax", "tanh", "rank", "none" @@ -473,7 +477,10 @@ def get_weighted_extrema( ): # "none" return extrema, default_weights - if strategy == WEIGHT_STRATEGIES[1]: # "threshold" + if strategy in ( + WEIGHT_STRATEGIES[1], + WEIGHT_STRATEGIES[2], + ): # "amplitude" or "amplitude_excess" extrema_weights = calculate_extrema_weights( series=extrema, indices=indices, @@ -902,10 +909,10 @@ def zigzag( df: pd.DataFrame, natr_period: int = 14, natr_ratio: float = 9.0, -) -> tuple[list[int], list[float], list[TrendDirection], list[float]]: +) -> tuple[list[int], list[float], list[TrendDirection], list[float], list[float]]: n = len(df) if df.empty or n < natr_period: - return [], [], [], [] + return [], [], [], [], [] natr_values = (ta.NATR(df, timeperiod=natr_period).bfill() / 100.0).to_numpy() @@ -921,7 +928,8 @@ def zigzag( pivots_indices: list[int] = [] pivots_values: list[float] = [] pivots_directions: list[TrendDirection] = [] - pivots_thresholds: list[float] = [] + pivots_amplitudes: list[float] = [] + pivots_amplitude_excesses: list[float] = [] last_pivot_pos: int = -1 candidate_pivot_pos: int = -1 @@ -971,7 +979,26 @@ def zigzag( pivots_indices.append(indices[pos]) pivots_values.append(value) pivots_directions.append(direction) - pivots_thresholds.append(thresholds[pos]) + if len(pivots_values) > 1: + prev_value = pivots_values[-2] + if np.isclose(prev_value, 0.0): + amplitude = np.nan + else: + amplitude = abs(value - prev_value) / abs(prev_value) + threshold_current = thresholds[pos] + if ( + np.isfinite(threshold_current) + and threshold_current > 0 + and np.isfinite(amplitude) + ): + amplitude_excess = amplitude / threshold_current + else: + amplitude_excess = np.nan + else: + amplitude = np.nan + amplitude_excess = np.nan + pivots_amplitudes.append(amplitude) + pivots_amplitude_excesses.append(amplitude_excess) last_pivot_pos = pos reset_candidate_pivot() @@ -1089,7 +1116,7 @@ def zigzag( state = TrendDirection.UP break else: - return [], [], [], [] + return [], [], [], [], [] for i in range(last_pivot_pos + 1, n): current_high = highs[i] @@ -1119,30 +1146,37 @@ def zigzag( ) state = TrendDirection.UP - return pivots_indices, pivots_values, pivots_directions, pivots_thresholds + return ( + pivots_indices, + pivots_values, + pivots_directions, + pivots_amplitudes, + pivots_amplitude_excesses, + ) -regressors = {"xgboost", "lightgbm"} +Regressor = Literal["xgboost", "lightgbm"] +REGRESSORS: Final[tuple[Regressor, ...]] = ("xgboost", "lightgbm") def get_optuna_callbacks( - trial: optuna.trial.Trial, regressor: str + trial: optuna.trial.Trial, regressor: Regressor ) -> list[Callable[[optuna.trial.Trial, str], None]]: - if regressor == "xgboost": + if regressor == REGRESSORS[0]: # "xgboost" callbacks = [ optuna.integration.XGBoostPruningCallback(trial, "validation_0-rmse") ] - elif regressor == "lightgbm": + elif regressor == REGRESSORS[1]: # "lightgbm" callbacks = [optuna.integration.LightGBMPruningCallback(trial, "rmse")] else: raise ValueError( - f"Unsupported regressor model: {regressor} (supported: {', '.join(regressors)})" + f"Unsupported regressor model: {regressor} (supported: {', '.join(REGRESSORS)})" ) return callbacks def fit_regressor( - regressor: str, + regressor: Regressor, X: pd.DataFrame, y: pd.DataFrame, train_weights: NDArray[np.floating], @@ -1153,7 +1187,7 @@ def fit_regressor( callbacks: Optional[list[Callable[[optuna.trial.Trial, str], None]]] = None, trial: Optional[optuna.trial.Trial] = None, ) -> Any: - if regressor == "xgboost": + if regressor == REGRESSORS[0]: # "xgboost" from xgboost import XGBRegressor model_training_parameters.setdefault("random_state", 1) @@ -1177,7 +1211,7 @@ def fit_regressor( sample_weight_eval_set=eval_weights, xgb_model=init_model, ) - elif regressor == "lightgbm": + elif regressor == REGRESSORS[1]: # "lightgbm" from lightgbm import LGBMRegressor model_training_parameters.setdefault("seed", 1) @@ -1200,21 +1234,21 @@ def fit_regressor( ) else: raise ValueError( - f"Unsupported regressor model: {regressor} (supported: {', '.join(regressors)})" + f"Unsupported regressor model: {regressor} (supported: {', '.join(REGRESSORS)})" ) return model def get_optuna_study_model_parameters( trial: optuna.trial.Trial, - regressor: str, + regressor: Regressor, model_training_best_parameters: dict[str, Any], space_reduction: bool, expansion_ratio: float, ) -> dict[str, Any]: - if regressor not in regressors: + if regressor not in set(REGRESSORS): raise ValueError( - f"Unsupported regressor model: {regressor} (supported: {', '.join(regressors)})" + f"Unsupported regressor model: {regressor} (supported: {', '.join(REGRESSORS)})" ) if not isinstance(expansion_ratio, (int, float)) or not ( 0.0 <= expansion_ratio <= 1.0 @@ -1305,7 +1339,7 @@ def get_optuna_study_model_parameters( "reg_lambda", ranges["reg_lambda"][0], ranges["reg_lambda"][1], log=True ), } - if regressor == "xgboost": + if regressor == REGRESSORS[0]: # "xgboost" study_model_parameters.update( { "max_depth": trial.suggest_int( @@ -1318,7 +1352,7 @@ def get_optuna_study_model_parameters( ), } ) - elif regressor == "lightgbm": + elif regressor == REGRESSORS[1]: # "lightgbm" study_model_parameters.update( { "num_leaves": trial.suggest_int(