]> Piment Noir Git Repositories - freqai-strategies.git/commitdiff
refactor!(qav3): cleanup extrema selection methods namespace
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Thu, 18 Dec 2025 20:28:46 +0000 (21:28 +0100)
committerJérôme Benoit <jerome.benoit@piment-noir.org>
Thu, 18 Dec 2025 20:28:46 +0000 (21:28 +0100)
Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
README.md
quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py
quickadapter/user_data/strategies/QuickAdapterV3.py

index f6c9f88ceb3b5f0fd04840974ab44149f3398917..b86a237e8cc43619a9e8619fba7215afbf65813c 100644 (file)
--- 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).                                                                                                              |
index 5dd820ed2756f169c0d74d3f485e71ce27807e60..c8a6e94b940d56700db17cd3bcd2d59599f35412 100644 (file)
@@ -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
             )
 
index 27f476d7b2a40ec4209b084b5503a1fef8939198..9ac60255563896f86abda7d0cb7f5f0aa0732535 100644 (file)
@@ -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}",