From 3723cc57a5e4e42ec4b0a4309ddcfe9b04253bd0 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Thu, 18 Dec 2025 21:28:46 +0100 Subject: [PATCH] refactor!(qav3): cleanup extrema selection methods namespace MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Signed-off-by: Jérôme Benoit --- README.md | 4 +- .../freqaimodels/QuickAdapterRegressorV3.py | 18 ++-- .../user_data/strategies/QuickAdapterV3.py | 88 +++++++++---------- 3 files changed, 53 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index f6c9f88..b86a237 100644 --- a/README.md +++ b/README.md @@ -100,11 +100,11 @@ docker compose up -d --build | freqai.feature_parameters.label_knn_p_order | `None` | float \| None | Tunable for KNN neighbor distances aggregation methods: p-order (`knn_power_mean`, default: 1.0) or quantile (`knn_quantile`, default: 0.5). (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 {`rank`,`values`,`partition`} | Extrema selection method. `rank` uses ranked extrema values, `values` uses ranked reversal values, `partition` uses sign-based partitioning. | +| freqai.predictions_extrema.selection_method | `rank_extrema` | enum {`rank_extrema`,`rank_peaks`,`partition`} | Extrema selection method. `rank_extrema` ranks extrema values, `rank_peaks` ranks detected peak values, `partition` uses sign-based partitioning. | | freqai.predictions_extrema.thresholds_smoothing | `mean` | enum {`mean`,`isodata`,`li`,`minimum`,`otsu`,`triangle`,`yen`,`median`,`soft_extremum`} | Thresholding method for prediction thresholds smoothing. | | freqai.predictions_extrema.thresholds_alpha | 12.0 | float > 0 | Alpha for `soft_extremum` thresholds smoothing. | | freqai.predictions_extrema.threshold_outlier | 0.999 | float (0,1) | Quantile threshold for predictions outlier filtering. | -| freqai.predictions_extrema.extrema_fraction | 1.0 | float (0,1] | Fraction of extrema used for thresholds. `1.0` uses all, lower values keep only most significant. Applies to `rank` and `values`; ignored for `partition`. | +| freqai.predictions_extrema.extrema_fraction | 1.0 | float (0,1] | Fraction of extrema used for thresholds. `1.0` uses all, lower values keep only most significant. Applies to `rank_extrema` and `rank_peaks`; ignored for `partition`. | | _Optuna / HPO_ | | | | | freqai.optuna_hyperopt.enabled | true | bool | Enables HPO. | | freqai.optuna_hyperopt.sampler | `tpe` | enum {`tpe`,`auto`} | HPO sampler algorithm. `tpe` uses [TPESampler](https://optuna.readthedocs.io/en/stable/reference/samplers/generated/optuna.samplers.TPESampler.html) with multivariate and group, `auto` uses [AutoSampler](https://hub.optuna.org/samplers/auto_sampler). | diff --git a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py index 5dd820e..c8a6e94 100644 --- a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py +++ b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py @@ -40,7 +40,7 @@ from Utils import ( zigzag, ) -ExtremaSelectionMethod = Literal["rank", "values", "partition"] +ExtremaSelectionMethod = Literal["rank_extrema", "rank_peaks", "partition"] OptunaNamespace = Literal["hp", "train", "label"] CustomThresholdMethod = Literal["median", "soft_extremum"] SkimageThresholdMethod = Literal[ @@ -79,8 +79,8 @@ class QuickAdapterRegressorV3(BaseRegressionModel): _SQRT_2: Final[float] = np.sqrt(2.0) _EXTREMA_SELECTION_METHODS: Final[tuple[ExtremaSelectionMethod, ...]] = ( - "rank", - "values", + "rank_extrema", + "rank_peaks", "partition", ) _CUSTOM_THRESHOLD_METHODS: Final[tuple[CustomThresholdMethod, ...]] = ( @@ -315,7 +315,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): selection_method = str( predictions_extrema.get( "selection_method", - QuickAdapterRegressorV3._EXTREMA_SELECTION_METHODS[0], # "rank" + QuickAdapterRegressorV3._EXTREMA_SELECTION_METHODS[0], # "rank_extrema" ) ) if ( @@ -506,7 +506,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): logger.info(f" label_weights: {label_weights}") else: logger.info( - " label_weights: [1.0, ...] * n_objectives, normalized (default)" + " label_weights: [1.0, ...] * n_objectives, l1 normalized (default)" ) label_p_order_config = self.ft_params.get("label_p_order") @@ -1263,7 +1263,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): return minima_indices, maxima_indices @staticmethod - def _get_extrema_values( + def _get_ranked_peaks( pred_extrema: pd.Series, minima_indices: NDArray[np.intp], maxima_indices: NDArray[np.intp], @@ -1331,7 +1331,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): if ( extrema_selection == QuickAdapterRegressorV3._EXTREMA_SELECTION_METHODS[0] - ): # "rank" + ): # "rank_extrema" minima_indices, maxima_indices = ( QuickAdapterRegressorV3._get_extrema_indices(pred_extrema) ) @@ -1344,11 +1344,11 @@ class QuickAdapterRegressorV3(BaseRegressionModel): elif ( extrema_selection == QuickAdapterRegressorV3._EXTREMA_SELECTION_METHODS[1] - ): # "values" + ): # "rank_peaks" minima_indices, maxima_indices = ( QuickAdapterRegressorV3._get_extrema_indices(pred_extrema) ) - pred_minima, pred_maxima = QuickAdapterRegressorV3._get_extrema_values( + pred_minima, pred_maxima = QuickAdapterRegressorV3._get_ranked_peaks( pred_extrema, minima_indices, maxima_indices, extrema_fraction ) diff --git a/quickadapter/user_data/strategies/QuickAdapterV3.py b/quickadapter/user_data/strategies/QuickAdapterV3.py index 27f476d..9ac6025 100644 --- a/quickadapter/user_data/strategies/QuickAdapterV3.py +++ b/quickadapter/user_data/strategies/QuickAdapterV3.py @@ -132,14 +132,17 @@ class QuickAdapterV3(IStrategy): position_adjustment_enable = True - # {stage: (natr_ratio_percent, stake_percent)} - partial_exit_stages: ClassVar[dict[int, tuple[float, float]]] = { - 0: (0.4858, 0.4), - 1: (0.6180, 0.3), - 2: (0.7640, 0.2), + # {stage: (natr_ratio_percent, stake_percent, color)} + partial_exit_stages: ClassVar[dict[int, tuple[float, float, str]]] = { + 0: (0.4858, 0.4, "lime"), + 1: (0.6180, 0.3, "yellow"), + 2: (0.7640, 0.2, "coral"), } - CUSTOM_STOPLOSS_NATR_RATIO_PERCENT: Final[float] = 0.7860 + # (natr_ratio_percent, stake_percent, color) + _FINAL_EXIT_STAGE: Final[tuple[float, float, str]] = (1.0, 1.0, "deepskyblue") + + _CUSTOM_STOPLOSS_NATR_RATIO_PERCENT: Final[float] = 0.7860 timeframe_minutes = timeframe_to_minutes(timeframe) minimal_roi = {str(timeframe_minutes * 864): -1} @@ -423,18 +426,24 @@ class QuickAdapterV3(IStrategy): logger.info("Custom Stoploss:") logger.info( - f" natr_ratio_percent: {format_number(QuickAdapterV3.CUSTOM_STOPLOSS_NATR_RATIO_PERCENT)}" + f" natr_ratio_percent: {format_number(QuickAdapterV3._CUSTOM_STOPLOSS_NATR_RATIO_PERCENT)}" ) logger.info("Partial Exit Stages:") for stage, ( natr_ratio_percent, stake_percent, + color, ) in QuickAdapterV3.partial_exit_stages.items(): logger.info( - f" stage {stage}: natr_ratio_percent={format_number(natr_ratio_percent)}, stake_percent={format_number(stake_percent)}" + f" stage {stage}: natr_ratio_percent={format_number(natr_ratio_percent)}, stake_percent={format_number(stake_percent)}, color={color}" ) + final_stage = max(QuickAdapterV3.partial_exit_stages.keys(), default=-1) + 1 + logger.info( + f"Final Exit Stage: stage {final_stage}: natr_ratio_percent={format_number(QuickAdapterV3._FINAL_EXIT_STAGE[0])}, stake_percent={format_number(QuickAdapterV3._FINAL_EXIT_STAGE[1])}, color={QuickAdapterV3._FINAL_EXIT_STAGE[2]}" + ) + logger.info("Protections:") if self.protections: for protection in self.protections: @@ -1478,7 +1487,7 @@ class QuickAdapterV3(IStrategy): return None stoploss_distance = self.get_stoploss_distance( - df, trade, current_rate, QuickAdapterV3.CUSTOM_STOPLOSS_NATR_RATIO_PERCENT + df, trade, current_rate, QuickAdapterV3._CUSTOM_STOPLOSS_NATR_RATIO_PERCENT ) if isna(stoploss_distance) or stoploss_distance <= 0: return None @@ -1503,7 +1512,7 @@ class QuickAdapterV3(IStrategy): natr_ratio_percent = ( QuickAdapterV3.partial_exit_stages[exit_stage][0] if exit_stage in QuickAdapterV3.partial_exit_stages - else 1.0 + else QuickAdapterV3._FINAL_EXIT_STAGE[0] ) take_profit_distance = self.get_take_profit_distance( df, trade, natr_ratio_percent @@ -1514,7 +1523,6 @@ class QuickAdapterV3(IStrategy): take_profit_price = ( trade.open_rate + (-1 if trade.is_short else 1) * take_profit_distance ) - self.safe_append_trade_take_profit_price(trade, take_profit_price, exit_stage) return take_profit_price @@ -1637,6 +1645,10 @@ class QuickAdapterV3(IStrategy): if isna(trade_take_profit_price): return None + self.safe_append_trade_take_profit_price( + trade, trade_take_profit_price, trade_exit_stage + ) + trade_partial_exit = QuickAdapterV3.can_take_profit( trade, current_rate, trade_take_profit_price ) @@ -2274,6 +2286,11 @@ class QuickAdapterV3(IStrategy): ) if isna(trade_take_profit_price): return None + + self.safe_append_trade_take_profit_price( + trade, trade_take_profit_price, trade_exit_stage + ) + trade_take_profit_exit = QuickAdapterV3.can_take_profit( trade, current_rate, trade_take_profit_price ) @@ -2466,45 +2483,30 @@ class QuickAdapterV3(IStrategy): open_trades = Trade.get_trades_proxy(pair=pair, is_open=True) - take_profit_stage_colors = { - 0: "lime", - 1: "yellow", - 2: "coral", - 3: "deepskyblue", - } - for trade in open_trades: if trade.open_date_utc > end_date: continue trade_exit_stage = self.get_trade_exit_stage(trade) - for take_profit_stage, ( - natr_ratio_percent, - _, - ) in self.partial_exit_stages.items(): + for take_profit_stage, (_, _, color) in self.partial_exit_stages.items(): if take_profit_stage < trade_exit_stage: continue - take_profit_distance = self.get_take_profit_distance( - dataframe, trade, natr_ratio_percent + partial_take_profit_price = self.get_take_profit_price( + dataframe, trade, take_profit_stage ) - if take_profit_distance is None or take_profit_distance <= 0: + if isna(partial_take_profit_price): continue - take_profit_price = ( - trade.open_rate - + (-1 if trade.is_short else 1) * take_profit_distance - ) - take_profit_line_annotation: AnnotationType = { "type": "line", "start": max(trade.open_date_utc, start_date), "end": end_date, - "y_start": take_profit_price, - "y_end": take_profit_price, - "color": take_profit_stage_colors.get(take_profit_stage, "silver"), + "y_start": partial_take_profit_price, + "y_end": partial_take_profit_price, + "color": color, "line_style": "solid", "width": 1, "label": f"Take Profit {take_profit_stage}", @@ -2512,25 +2514,19 @@ class QuickAdapterV3(IStrategy): } annotations.append(take_profit_line_annotation) - final_stage = 3 - final_natr_ratio_percent = 1.0 - take_profit_distance = self.get_take_profit_distance( - dataframe, trade, final_natr_ratio_percent + final_stage = max(self.partial_exit_stages.keys(), default=-1) + 1 + final_take_profit_price = self.get_take_profit_price( + dataframe, trade, final_stage ) - if take_profit_distance is not None and take_profit_distance > 0: - take_profit_price = ( - trade.open_rate - + (-1 if trade.is_short else 1) * take_profit_distance - ) - + if not isna(final_take_profit_price): take_profit_line_annotation: AnnotationType = { "type": "line", "start": max(trade.open_date_utc, start_date), "end": end_date, - "y_start": take_profit_price, - "y_end": take_profit_price, - "color": take_profit_stage_colors.get(final_stage, "silver"), + "y_start": final_take_profit_price, + "y_end": final_take_profit_price, + "color": QuickAdapterV3._FINAL_EXIT_STAGE[2], "line_style": "solid", "width": 1, "label": f"Take Profit {final_stage}", -- 2.43.0