]> Piment Noir Git Repositories - freqai-strategies.git/commitdiff
refactor(qav3): log model and strategy specific configuration
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Tue, 16 Dec 2025 21:41:22 +0000 (22:41 +0100)
committerJérôme Benoit <jerome.benoit@piment-noir.org>
Tue, 16 Dec 2025 21:41:22 +0000 (22:41 +0100)
Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py
quickadapter/user_data/strategies/QuickAdapterV3.py
quickadapter/user_data/strategies/Utils.py

index 67e5e341c403aa7539e144fc6f0dc32754c0ab05..46390d1763e21c5b40957fcc0ff89ac4b3d4b387 100644 (file)
@@ -22,6 +22,7 @@ from optuna.study.study import ObjectiveFuncType
 from sklearn_extra.cluster import KMedoids
 
 from Utils import (
+    DEFAULT_FIT_LIVE_PREDICTIONS_CANDLES,
     EXTREMA_COLUMN,
     MAXIMA_THRESHOLD_COLUMN,
     MINIMA_THRESHOLD_COLUMN,
@@ -49,8 +50,6 @@ ThresholdMethod = Union[SkimageThresholdMethod, CustomThresholdMethod]
 
 debug = False
 
-TEST_SIZE: Final = 0.1
-
 warnings.simplefilter(action="ignore", category=FutureWarning)
 
 logger = logging.getLogger(__name__)
@@ -73,7 +72,9 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
     https://github.com/sponsors/robcaulk
     """
 
-    version = "3.7.133"
+    version = "3.7.134"
+
+    _TEST_SIZE: Final[float] = 0.1
 
     _SQRT_2: Final[float] = np.sqrt(2.0)
 
@@ -153,6 +154,19 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
         *_CUSTOM_METRICS,
     )
 
+    PREDICTIONS_EXTREMA_THRESHOLD_OUTLIER_DEFAULT: Final[float] = 0.999
+    PREDICTIONS_EXTREMA_THRESHOLDS_ALPHA_DEFAULT: Final[float] = 12.0
+    PREDICTIONS_EXTREMA_EXTREMA_FRACTION_DEFAULT: Final[float] = 1.0
+
+    FIT_LIVE_PREDICTIONS_CANDLES_DEFAULT: Final[int] = (
+        DEFAULT_FIT_LIVE_PREDICTIONS_CANDLES
+    )
+    MIN_LABEL_PERIOD_CANDLES_DEFAULT: Final[int] = 12
+    MAX_LABEL_PERIOD_CANDLES_DEFAULT: Final[int] = 24
+    MIN_LABEL_NATR_RATIO_DEFAULT: Final[float] = 9.0
+    MAX_LABEL_NATR_RATIO_DEFAULT: Final[float] = 12.0
+    LABEL_KNN_N_NEIGHBORS_DEFAULT: Final[int] = 5
+
     @staticmethod
     def _extrema_selection_methods_set() -> set[ExtremaSelectionMethod]:
         return set(QuickAdapterRegressorV3._EXTREMA_SELECTION_METHODS)
@@ -185,6 +199,22 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
     def _metrics_set() -> set[str]:
         return set(QuickAdapterRegressorV3._METRICS)
 
+    @staticmethod
+    def _get_label_p_order_default(metric: str) -> Optional[float]:
+        if metric == QuickAdapterRegressorV3._SCIPY_METRICS[5]:  # "minkowski"
+            return 2.0
+        elif metric == QuickAdapterRegressorV3._CUSTOM_METRICS[7]:  # "power_mean"
+            return 1.0
+        return None
+
+    @staticmethod
+    def _get_label_knn_p_order_default(metric: str) -> Optional[float]:
+        if metric == QuickAdapterRegressorV3._CUSTOM_METRICS[12]:  # "knn_power_mean"
+            return 1.0
+        elif metric == QuickAdapterRegressorV3._CUSTOM_METRICS[13]:  # "knn_quantile"
+            return 0.5
+        return None
+
     @cached_property
     def _optuna_config(self) -> dict[str, Any]:
         optuna_default_config = {
@@ -238,36 +268,30 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
 
         if label_frequency_candles is None:
             label_frequency_candles = default_label_frequency_candles
-            logger.info(f"{label_frequency_candles=} (default)")
         elif isinstance(label_frequency_candles, str):
             if label_frequency_candles == "auto":
                 label_frequency_candles = default_label_frequency_candles
-                logger.info(f"{label_frequency_candles=} (auto)")
             else:
                 logger.warning(
                     f"Invalid string value for label_frequency_candles: '{label_frequency_candles}'. "
                     f"Only 'auto' is supported. Using fallback"
                 )
                 label_frequency_candles = default_label_frequency_candles
-                logger.info(f"{label_frequency_candles=} (fallback)")
         elif isinstance(label_frequency_candles, (int, float)):
             if label_frequency_candles >= 2 and label_frequency_candles <= 10000:
                 label_frequency_candles = int(label_frequency_candles)
-                logger.info(f"{label_frequency_candles=} (configuration)")
             else:
                 logger.warning(
                     f"Invalid numeric value for label_frequency_candles: {label_frequency_candles}. "
                     f"Must be between 2 and 10000. Using fallback"
                 )
                 label_frequency_candles = default_label_frequency_candles
-                logger.info(f"{label_frequency_candles=} (fallback)")
         else:
             logger.warning(
                 f"Invalid type for label_frequency_candles: {type(label_frequency_candles).__name__}. "
                 f"Expected int, float, or 'auto'. Using fallback"
             )
             label_frequency_candles = default_label_frequency_candles
-            logger.info(f"{label_frequency_candles=} (fallback)")
 
         return label_frequency_candles
 
@@ -277,11 +301,16 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
         if not isinstance(predictions_extrema, dict):
             predictions_extrema = {}
 
-        threshold_outlier = predictions_extrema.get("threshold_outlier", 0.999)
+        threshold_outlier = predictions_extrema.get(
+            "threshold_outlier",
+            QuickAdapterRegressorV3.PREDICTIONS_EXTREMA_THRESHOLD_OUTLIER_DEFAULT,
+        )
         if not isinstance(threshold_outlier, (int, float)) or not (
             0 < threshold_outlier < 1
         ):
-            threshold_outlier = 0.999
+            threshold_outlier = (
+                QuickAdapterRegressorV3.PREDICTIONS_EXTREMA_THRESHOLD_OUTLIER_DEFAULT
+            )
 
         selection_method = str(
             predictions_extrema.get(
@@ -306,15 +335,25 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
                 0
             ]  # "mean"
 
-        thresholds_alpha = predictions_extrema.get("thresholds_alpha", 12.0)
+        thresholds_alpha = predictions_extrema.get(
+            "thresholds_alpha",
+            QuickAdapterRegressorV3.PREDICTIONS_EXTREMA_THRESHOLDS_ALPHA_DEFAULT,
+        )
         if not isinstance(thresholds_alpha, (int, float)) or thresholds_alpha < 0:
-            thresholds_alpha = 12.0
+            thresholds_alpha = (
+                QuickAdapterRegressorV3.PREDICTIONS_EXTREMA_THRESHOLDS_ALPHA_DEFAULT
+            )
 
-        extrema_fraction = predictions_extrema.get("extrema_fraction", 1.0)
+        extrema_fraction = predictions_extrema.get(
+            "extrema_fraction",
+            QuickAdapterRegressorV3.PREDICTIONS_EXTREMA_EXTREMA_FRACTION_DEFAULT,
+        )
         if not isinstance(extrema_fraction, (int, float)) or not (
             0 < extrema_fraction <= 1
         ):
-            extrema_fraction = 1.0
+            extrema_fraction = (
+                QuickAdapterRegressorV3.PREDICTIONS_EXTREMA_EXTREMA_FRACTION_DEFAULT
+            )
 
         return {
             "threshold_outlier": float(threshold_outlier),
@@ -355,7 +394,10 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
         self._optuna_hyperopt: Optional[bool] = (
             self.freqai_info.get("enabled", False)
             and self._optuna_config.get("enabled")
-            and self.data_split_parameters.get("test_size", TEST_SIZE) > 0
+            and self.data_split_parameters.get(
+                "test_size", QuickAdapterRegressorV3._TEST_SIZE
+            )
+            > 0
         )
         self._optuna_hp_value: dict[str, float] = {}
         self._optuna_train_value: dict[str, float] = {}
@@ -421,10 +463,299 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
         if self.regressor not in set(REGRESSORS):
             self.regressor = REGRESSORS[0]
             self.freqai_info["regressor"] = self.regressor
+        self._log_model_configuration()
+
+    def _log_model_configuration(self) -> None:
+        logger.info("=" * 60)
+        logger.info("QuickAdapterRegressor Configuration")
+        logger.info("=" * 60)
+
+        logger.info(f"Model Version: {self.version}")
+        logger.info(f"Regressor: {self.regressor}")
+
+        logger.info("Optuna Hyperopt Configuration:")
+        optuna_config = self._optuna_config
+        logger.info(f"  enabled: {optuna_config.get('enabled')}")
+        if optuna_config.get("enabled"):
+            logger.info(f"  n_jobs: {optuna_config.get('n_jobs')}")
+            logger.info(f"  sampler: {optuna_config.get('sampler')}")
+            logger.info(f"  storage: {optuna_config.get('storage')}")
+            logger.info(f"  continuous: {optuna_config.get('continuous')}")
+            logger.info(f"  warm_start: {optuna_config.get('warm_start')}")
+            logger.info(f"  n_startup_trials: {optuna_config.get('n_startup_trials')}")
+            logger.info(f"  n_trials: {optuna_config.get('n_trials')}")
+            logger.info(f"  timeout: {optuna_config.get('timeout')}")
+            logger.info(
+                f"  label_candles_step: {optuna_config.get('label_candles_step')}"
+            )
+            logger.info(
+                f"  train_candles_step: {optuna_config.get('train_candles_step')}"
+            )
+            logger.info(f"  space_reduction: {optuna_config.get('space_reduction')}")
+            logger.info(
+                f"  expansion_ratio: {format_number(optuna_config.get('expansion_ratio'))}"
+            )
+            logger.info(f"  min_resource: {optuna_config.get('min_resource')}")
+            logger.info(f"  seed: {optuna_config.get('seed')}")
+            logger.info(
+                f"  label_metric: {self.ft_params.get('label_metric', QuickAdapterRegressorV3._SCIPY_METRICS[2])}"
+            )
+
+            label_weights = self.ft_params.get("label_weights")
+            if label_weights is not None:
+                logger.info(f"  label_weights: {label_weights}")
+            else:
+                logger.info(
+                    "  label_weights: [1.0, ...] * n_objectives, normalized (default)"
+                )
+
+            label_p_order_config = self.ft_params.get("label_p_order")
+            label_metric = self.ft_params.get(
+                "label_metric", QuickAdapterRegressorV3._SCIPY_METRICS[2]
+            )
+
+            label_p_order_is_used = False
+            label_p_order_reason = None
+
+            if label_metric in {
+                QuickAdapterRegressorV3._SCIPY_METRICS[5],  # "minkowski"
+                QuickAdapterRegressorV3._CUSTOM_METRICS[7],  # "power_mean"
+            }:
+                label_p_order_is_used = True
+                label_p_order_reason = label_metric
+            elif (
+                label_metric == QuickAdapterRegressorV3._CUSTOM_METRICS[16]
+            ):  # "medoid"
+                label_medoid_metric = self.ft_params.get(
+                    "label_medoid_metric",
+                    QuickAdapterRegressorV3._SCIPY_METRICS[2],  # "euclidean" default
+                )
+                if (
+                    label_medoid_metric == QuickAdapterRegressorV3._SCIPY_METRICS[5]
+                ):  # "minkowski"
+                    label_p_order_is_used = True
+                    label_p_order_reason = f"{label_metric} (via label_medoid_metric={label_medoid_metric})"
+            elif label_metric in {
+                QuickAdapterRegressorV3._CUSTOM_METRICS[9],  # "kmeans"
+                QuickAdapterRegressorV3._CUSTOM_METRICS[10],  # "kmeans2"
+            }:
+                label_kmeans_metric = self.ft_params.get(
+                    "label_kmeans_metric",
+                    QuickAdapterRegressorV3._SCIPY_METRICS[2],  # "euclidean" default
+                )
+                if (
+                    label_kmeans_metric == QuickAdapterRegressorV3._SCIPY_METRICS[5]
+                ):  # "minkowski"
+                    label_p_order_is_used = True
+                    label_p_order_reason = f"{label_metric} (via label_kmeans_metric={label_kmeans_metric})"
+            elif (
+                label_metric == QuickAdapterRegressorV3._CUSTOM_METRICS[11]
+            ):  # "kmedoids"
+                label_kmedoids_metric = self.ft_params.get(
+                    "label_kmedoids_metric",
+                    QuickAdapterRegressorV3._SCIPY_METRICS[2],  # "euclidean" default
+                )
+                if (
+                    label_kmedoids_metric == QuickAdapterRegressorV3._SCIPY_METRICS[5]
+                ):  # "minkowski"
+                    label_p_order_is_used = True
+                    label_p_order_reason = f"{label_metric} (via label_kmedoids_metric={label_kmedoids_metric})"
+            elif label_metric in {
+                QuickAdapterRegressorV3._CUSTOM_METRICS[12],  # "knn_power_mean"
+                QuickAdapterRegressorV3._CUSTOM_METRICS[13],  # "knn_quantile"
+                QuickAdapterRegressorV3._CUSTOM_METRICS[14],  # "knn_min"
+                QuickAdapterRegressorV3._CUSTOM_METRICS[15],  # "knn_max"
+            }:
+                label_knn_metric = self.ft_params.get(
+                    "label_knn_metric",
+                    QuickAdapterRegressorV3._SCIPY_METRICS[5],  # "minkowski" default
+                )
+                if (
+                    label_knn_metric == QuickAdapterRegressorV3._SCIPY_METRICS[5]
+                ):  # "minkowski"
+                    label_p_order_is_used = True
+                    label_p_order_reason = (
+                        f"{label_metric} (via label_knn_metric={label_knn_metric})"
+                    )
+
+            if label_p_order_config is not None:
+                logger.info(
+                    f"  label_p_order: {format_number(float(label_p_order_config))}"
+                )
+            elif label_p_order_is_used:
+                if label_metric in {
+                    QuickAdapterRegressorV3._SCIPY_METRICS[5],  # "minkowski"
+                    QuickAdapterRegressorV3._CUSTOM_METRICS[7],  # "power_mean"
+                }:
+                    label_p_order_default = (
+                        QuickAdapterRegressorV3._get_label_p_order_default(label_metric)
+                    )
+                else:
+                    label_p_order_default = (
+                        QuickAdapterRegressorV3._get_label_p_order_default(
+                            QuickAdapterRegressorV3._SCIPY_METRICS[
+                                5
+                            ]  # "minkowski" default
+                        )
+                    )
+                logger.info(
+                    f"  label_p_order: {format_number(label_p_order_default)} (default for {label_p_order_reason})"
+                )
+
+            label_medoid_metric_config = self.ft_params.get("label_medoid_metric")
+            if label_medoid_metric_config is not None:
+                logger.info(f"  label_medoid_metric: {label_medoid_metric_config}")
+            elif (
+                label_metric == QuickAdapterRegressorV3._CUSTOM_METRICS[16]
+            ):  # "medoid"
+                logger.info(
+                    f"  label_medoid_metric: {QuickAdapterRegressorV3._SCIPY_METRICS[2]} (default for {label_metric})"
+                )
+            label_kmeans_metric_config = self.ft_params.get("label_kmeans_metric")
+            if label_kmeans_metric_config is not None:
+                logger.info(f"  label_kmeans_metric: {label_kmeans_metric_config}")
+            elif label_metric in {
+                QuickAdapterRegressorV3._CUSTOM_METRICS[9],  # "kmeans"
+                QuickAdapterRegressorV3._CUSTOM_METRICS[10],  # "kmeans2"
+            }:
+                logger.info(
+                    f"  label_kmeans_metric: {QuickAdapterRegressorV3._SCIPY_METRICS[2]} (default for {label_metric})"
+                )
+
+            label_kmeans_selection_config = self.ft_params.get("label_kmeans_selection")
+            if label_kmeans_selection_config is not None:
+                logger.info(
+                    f"  label_kmeans_selection: {label_kmeans_selection_config}"
+                )
+            elif label_metric in {
+                QuickAdapterRegressorV3._CUSTOM_METRICS[9],  # "kmeans"
+                QuickAdapterRegressorV3._CUSTOM_METRICS[10],  # "kmeans2"
+            }:
+                logger.info(
+                    f"  label_kmeans_selection: min (default for {label_metric})"
+                )
+            label_kmedoids_metric_config = self.ft_params.get("label_kmedoids_metric")
+            if label_kmedoids_metric_config is not None:
+                logger.info(f"  label_kmedoids_metric: {label_kmedoids_metric_config}")
+            elif (
+                label_metric == QuickAdapterRegressorV3._CUSTOM_METRICS[11]
+            ):  # "kmedoids"
+                logger.info(
+                    f"  label_kmedoids_metric: {QuickAdapterRegressorV3._SCIPY_METRICS[2]} (default for {label_metric})"
+                )
+
+            label_kmedoids_selection_config = self.ft_params.get(
+                "label_kmedoids_selection"
+            )
+            if label_kmedoids_selection_config is not None:
+                logger.info(
+                    f"  label_kmedoids_selection: {label_kmedoids_selection_config}"
+                )
+            elif (
+                label_metric == QuickAdapterRegressorV3._CUSTOM_METRICS[11]
+            ):  # "kmedoids"
+                logger.info(
+                    f"  label_kmedoids_selection: min (default for {label_metric})"
+                )
+
+            label_knn_metric_config = self.ft_params.get("label_knn_metric")
+            if label_knn_metric_config is not None:
+                logger.info(f"  label_knn_metric: {label_knn_metric_config}")
+            elif label_metric in {
+                QuickAdapterRegressorV3._CUSTOM_METRICS[12],  # "knn_power_mean"
+                QuickAdapterRegressorV3._CUSTOM_METRICS[13],  # "knn_quantile"
+                QuickAdapterRegressorV3._CUSTOM_METRICS[14],  # "knn_min"
+                QuickAdapterRegressorV3._CUSTOM_METRICS[15],  # "knn_max"
+            }:
+                logger.info(
+                    f"  label_knn_metric: {QuickAdapterRegressorV3._SCIPY_METRICS[5]} (default for {label_metric})"
+                )
+
+            label_knn_n_neighbors = self.ft_params.get("label_knn_n_neighbors")
+            if label_knn_n_neighbors is not None:
+                logger.info(f"  label_knn_n_neighbors: {label_knn_n_neighbors}")
+            elif label_metric in {
+                QuickAdapterRegressorV3._CUSTOM_METRICS[12],  # "knn_power_mean"
+                QuickAdapterRegressorV3._CUSTOM_METRICS[13],  # "knn_quantile"
+                QuickAdapterRegressorV3._CUSTOM_METRICS[14],  # "knn_min"
+                QuickAdapterRegressorV3._CUSTOM_METRICS[15],  # "knn_max"
+            }:
+                logger.info(
+                    f"  label_knn_n_neighbors: {QuickAdapterRegressorV3.LABEL_KNN_N_NEIGHBORS_DEFAULT} (default for {label_metric})"
+                )
+
+            label_knn_p_order_config = self.ft_params.get("label_knn_p_order")
+            if label_knn_p_order_config is not None:
+                logger.info(
+                    f"  label_knn_p_order: {format_number(float(label_knn_p_order_config))}"
+                )
+            elif label_metric in {
+                QuickAdapterRegressorV3._CUSTOM_METRICS[12],  # "knn_power_mean"
+                QuickAdapterRegressorV3._CUSTOM_METRICS[13],  # "knn_quantile"
+            }:
+                label_knn_p_order_default = (
+                    QuickAdapterRegressorV3._get_label_knn_p_order_default(label_metric)
+                )
+                logger.info(
+                    f"  label_knn_p_order: {format_number(label_knn_p_order_default)} (default for {label_metric})"
+                )
+
+        logger.info("Predictions Extrema Configuration:")
+        predictions_extrema = self.predictions_extrema
         logger.info(
-            f"Initialized {self.__class__.__name__} {self.regressor} regressor model version {self.version}"
+            f"  selection_method: {predictions_extrema.get('selection_method')}"
+        )
+        logger.info(
+            f"  thresholds_smoothing: {predictions_extrema.get('thresholds_smoothing')}"
+        )
+        logger.info(
+            f"  threshold_outlier: {format_number(predictions_extrema.get('threshold_outlier'))}"
+        )
+        logger.info(
+            f"  thresholds_alpha: {format_number(predictions_extrema.get('thresholds_alpha'))}"
+        )
+        logger.info(
+            f"  extrema_fraction: {format_number(predictions_extrema.get('extrema_fraction'))}"
+        )
+
+        logger.info("Label Configuration:")
+        logger.info(
+            f"  fit_live_predictions_candles: {self.freqai_info.get('fit_live_predictions_candles', QuickAdapterRegressorV3.FIT_LIVE_PREDICTIONS_CANDLES_DEFAULT)}"
+        )
+        logger.info(f"  label_frequency_candles: {self._label_frequency_candles}")
+        logger.info(
+            f"  min_label_period_candles: {self.ft_params.get('min_label_period_candles', QuickAdapterRegressorV3.MIN_LABEL_PERIOD_CANDLES_DEFAULT)}"
+        )
+        logger.info(
+            f"  max_label_period_candles: {self.ft_params.get('max_label_period_candles', QuickAdapterRegressorV3.MAX_LABEL_PERIOD_CANDLES_DEFAULT)}"
+        )
+        logger.info(
+            f"  min_label_natr_ratio: {format_number(self.ft_params.get('min_label_natr_ratio', QuickAdapterRegressorV3.MIN_LABEL_NATR_RATIO_DEFAULT))}"
+        )
+        logger.info(
+            f"  max_label_natr_ratio: {format_number(self.ft_params.get('max_label_natr_ratio', QuickAdapterRegressorV3.MAX_LABEL_NATR_RATIO_DEFAULT))}"
         )
 
+        if self._optuna_hyperopt:
+            logger.info("Label Parameters:")
+            for pair in self.pairs:
+                params = self._optuna_label_params.get(pair, {})
+                if params:
+                    logger.info(
+                        f"  {pair}: label_period_candles={params.get('label_period_candles')}, "
+                        f"label_natr_ratio={format_number(params.get('label_natr_ratio'))}"
+                    )
+        else:
+            logger.info("Label Parameters:")
+            logger.info(
+                f"  label_period_candles: {self.ft_params.get('label_period_candles', self._default_label_period_candles)}"
+            )
+            logger.info(
+                f"  label_natr_ratio: {format_number(float(self.ft_params.get('label_natr_ratio', self._default_label_natr_ratio)))}"
+            )
+
+        logger.info("=" * 60)
+
     def get_optuna_params(self, pair: str, namespace: str) -> dict[str, Any]:
         if namespace == QuickAdapterRegressorV3._OPTUNA_NAMESPACES[0]:  # "hp"
             params = self._optuna_hp_params.get(pair)
@@ -612,8 +943,13 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
                     X_test,
                     y_test,
                     test_weights,
-                    self.data_split_parameters.get("test_size", TEST_SIZE),
-                    self.freqai_info.get("fit_live_predictions_candles", 100),
+                    self.data_split_parameters.get(
+                        "test_size", QuickAdapterRegressorV3._TEST_SIZE
+                    ),
+                    self.freqai_info.get(
+                        "fit_live_predictions_candles",
+                        QuickAdapterRegressorV3.FIT_LIVE_PREDICTIONS_CANDLES_DEFAULT,
+                    ),
                     self._optuna_config.get("train_candles_step"),
                     model_training_parameters,
                 ),
@@ -656,7 +992,9 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
             X_test,
             y_test,
             test_weights,
-            self.data_split_parameters.get("test_size", TEST_SIZE),
+            self.data_split_parameters.get(
+                "test_size", QuickAdapterRegressorV3._TEST_SIZE
+            ),
         )
 
         model = fit_regressor(
@@ -717,7 +1055,8 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
         warmed_up = True
 
         fit_live_predictions_candles = self.freqai_info.get(
-            "fit_live_predictions_candles", 100
+            "fit_live_predictions_candles",
+            QuickAdapterRegressorV3.FIT_LIVE_PREDICTIONS_CANDLES_DEFAULT,
         )
 
         if self._optuna_hyperopt:
@@ -735,16 +1074,20 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
                         fit_live_predictions_candles,
                         self._optuna_config.get("label_candles_step"),
                         min_label_period_candles=self.ft_params.get(
-                            "min_label_period_candles", 12
+                            "min_label_period_candles",
+                            QuickAdapterRegressorV3.MIN_LABEL_PERIOD_CANDLES_DEFAULT,
                         ),
                         max_label_period_candles=self.ft_params.get(
-                            "max_label_period_candles", 24
+                            "max_label_period_candles",
+                            QuickAdapterRegressorV3.MAX_LABEL_PERIOD_CANDLES_DEFAULT,
                         ),
                         min_label_natr_ratio=self.ft_params.get(
-                            "min_label_natr_ratio", 9.0
+                            "min_label_natr_ratio",
+                            QuickAdapterRegressorV3.MIN_LABEL_NATR_RATIO_DEFAULT,
                         ),
                         max_label_natr_ratio=self.ft_params.get(
-                            "max_label_natr_ratio", 12.0
+                            "max_label_natr_ratio",
+                            QuickAdapterRegressorV3.MAX_LABEL_NATR_RATIO_DEFAULT,
                         ),
                     ),
                     directions=[
@@ -1399,7 +1742,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
                 cdist_kwargs["p"] = (
                     label_p_order
                     if label_p_order is not None and np.isfinite(label_p_order)
-                    else 2.0
+                    else self._get_label_p_order_default(metric)
                 )
             return sp.spatial.distance.cdist(
                 normalized_matrix,
@@ -1447,7 +1790,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
                     7
                 ]: label_p_order  # "power_mean"
                 if label_p_order is not None and np.isfinite(label_p_order)
-                else 1.0,
+                else self._get_label_p_order_default(metric),
             }[metric]
             return sp.stats.pmean(
                 ideal_point, p=p, weights=np_weights
@@ -1475,7 +1818,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
                 p = (
                     label_p_order
                     if label_p_order is not None and np.isfinite(label_p_order)
-                    else 2.0
+                    else self._get_label_p_order_default(label_medoid_metric)
                 )
             return self._pairwise_distance_sums(
                 normalized_matrix,
@@ -1518,7 +1861,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
                 cdist_kwargs["p"] = (
                     label_p_order
                     if label_p_order is not None and np.isfinite(label_p_order)
-                    else 2.0
+                    else self._get_label_p_order_default(label_kmeans_metric)
                 )
             cluster_center_distances_to_ideal = sp.spatial.distance.cdist(
                 cluster_centers,
@@ -1548,7 +1891,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
                         p = (
                             label_p_order
                             if label_p_order is not None and np.isfinite(label_p_order)
-                            else 2.0
+                            else self._get_label_p_order_default(label_kmeans_metric)
                         )
                     best_medoid_position = np.nanargmin(
                         self._pairwise_distance_sums(
@@ -1613,7 +1956,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
                 cdist_kwargs["p"] = (
                     label_p_order
                     if label_p_order is not None and np.isfinite(label_p_order)
-                    else 2.0
+                    else self._get_label_p_order_default(label_kmedoids_metric)
                 )
             medoid_distances_to_ideal = sp.spatial.distance.cdist(
                 normalized_matrix[medoid_indices],
@@ -1678,13 +2021,18 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
                 knn_kwargs["p"] = (
                     label_p_order
                     if label_p_order is not None and np.isfinite(label_p_order)
-                    else 2.0
+                    else self._get_label_p_order_default(label_knn_metric)
                 )
                 knn_kwargs["metric_params"] = {"w": np_weights}
             label_knn_p_order = self.ft_params.get("label_knn_p_order")
             n_neighbors = (
                 min(
-                    int(self.ft_params.get("label_knn_n_neighbors", 5)),
+                    int(
+                        self.ft_params.get(
+                            "label_knn_n_neighbors",
+                            QuickAdapterRegressorV3.LABEL_KNN_N_NEIGHBORS_DEFAULT,
+                        )
+                    ),
                     n_samples - 1,
                 )
                 + 1
@@ -1702,7 +2050,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
                 label_knn_p_order = (
                     label_knn_p_order
                     if label_knn_p_order is not None and np.isfinite(label_knn_p_order)
-                    else 1.0
+                    else self._get_label_knn_p_order_default(metric)
                 )
                 return sp.stats.pmean(neighbor_distances, p=label_knn_p_order, axis=1)
             elif (
@@ -1711,7 +2059,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
                 label_knn_p_order = (
                     label_knn_p_order
                     if label_knn_p_order is not None and np.isfinite(label_knn_p_order)
-                    else 0.5
+                    else self._get_label_knn_p_order_default(metric)
                 )
                 return np.nanquantile(neighbor_distances, label_knn_p_order, axis=1)
             elif metric == QuickAdapterRegressorV3._CUSTOM_METRICS[14]:  # "knn_min"
@@ -2260,10 +2608,10 @@ def label_objective(
     df: pd.DataFrame,
     fit_live_predictions_candles: int,
     candles_step: int,
-    min_label_period_candles: int = 12,
-    max_label_period_candles: int = 24,
-    min_label_natr_ratio: float = 9.0,
-    max_label_natr_ratio: float = 12.0,
+    min_label_period_candles: int = QuickAdapterRegressorV3.MIN_LABEL_PERIOD_CANDLES_DEFAULT,
+    max_label_period_candles: int = QuickAdapterRegressorV3.MAX_LABEL_PERIOD_CANDLES_DEFAULT,
+    min_label_natr_ratio: float = QuickAdapterRegressorV3.MIN_LABEL_NATR_RATIO_DEFAULT,
+    max_label_natr_ratio: float = QuickAdapterRegressorV3.MAX_LABEL_NATR_RATIO_DEFAULT,
 ) -> tuple[int, float, float, float, float, float]:
     min_label_period_candles, max_label_period_candles, candles_step = (
         get_min_max_label_period_candles(
index 1d5f4ad1aa3a2ec5632e175e5837a54d5749d0ed..791a125844c9b8db951f9e3a3b55394f7312dd76 100644 (file)
@@ -27,6 +27,7 @@ from scipy.stats import t
 from technical.pivots_points import pivots_points
 
 from Utils import (
+    DEFAULT_FIT_LIVE_PREDICTIONS_CANDLES,
     DEFAULTS_EXTREMA_SMOOTHING,
     DEFAULTS_EXTREMA_WEIGHTING,
     EXTREMA_COLUMN,
@@ -40,7 +41,6 @@ from Utils import (
     SMOOTHING_MODES,
     STANDARDIZATION_TYPES,
     WEIGHT_STRATEGIES,
-    TrendDirection,
     alligator,
     bottom_change_percent,
     calculate_n_extrema,
@@ -107,7 +107,7 @@ class QuickAdapterV3(IStrategy):
     _TRADING_MODES: Final[tuple[TradingMode, ...]] = ("spot", "margin", "futures")
 
     def version(self) -> str:
-        return "3.3.183"
+        return "3.3.184"
 
     timeframe = "5m"
 
@@ -139,6 +139,8 @@ class QuickAdapterV3(IStrategy):
         2: (0.7640, 0.2),
     }
 
+    CUSTOM_STOPLOSS_NATR_RATIO_PERCENT: Final[float] = 0.7860
+
     timeframe_minutes = timeframe_to_minutes(timeframe)
     minimal_roi = {str(timeframe_minutes * 864): -1}
 
@@ -147,7 +149,7 @@ class QuickAdapterV3(IStrategy):
     # def minimal_roi(self) -> dict[str, Any]:
     #     timeframe_minutes = timeframe_to_minutes(self.config.get("timeframe", "5m"))
     #     fit_live_predictions_candles = int(
-    #         self.config.get("freqai", {}).get("fit_live_predictions_candles", 100)
+    #         self.config.get("freqai", {}).get("fit_live_predictions_candles", DEFAULT_FIT_LIVE_PREDICTIONS_CANDLES)
     #     )
     #     return {str(timeframe_minutes * fit_live_predictions_candles): -1}
 
@@ -196,7 +198,9 @@ class QuickAdapterV3(IStrategy):
     @cached_property
     def protections(self) -> list[dict[str, Any]]:
         fit_live_predictions_candles = int(
-            self.config.get("freqai", {}).get("fit_live_predictions_candles", 100)
+            self.config.get("freqai", {}).get(
+                "fit_live_predictions_candles", DEFAULT_FIT_LIVE_PREDICTIONS_CANDLES
+            )
         )
         protections = self.config.get("custom_protections", {})
         trade_duration_candles = int(protections.get("trade_duration_candles", 72))
@@ -269,7 +273,9 @@ class QuickAdapterV3(IStrategy):
     @cached_property
     def startup_candle_count(self) -> int:
         # Match the predictions warmup period
-        return self.config.get("freqai", {}).get("fit_live_predictions_candles", 100)
+        return self.config.get("freqai", {}).get(
+            "fit_live_predictions_candles", DEFAULT_FIT_LIVE_PREDICTIONS_CANDLES
+        )
 
     @cached_property
     def max_open_trades_per_side(self) -> int:
@@ -355,6 +361,96 @@ class QuickAdapterV3(IStrategy):
         self._candle_threshold_cache: dict[CandleThresholdCacheKey, float] = {}
         self._cached_df_signature: dict[str, DfSignature] = {}
 
+        self._log_strategy_configuration()
+
+    def _log_strategy_configuration(self) -> None:
+        logger.info("=" * 60)
+        logger.info("QuickAdapter Strategy Configuration")
+        logger.info("=" * 60)
+
+        logger.info("Extrema Weighting:")
+        logger.info(f"  strategy: {self.extrema_weighting['strategy']}")
+        logger.info(f"  source_weights: {self.extrema_weighting['source_weights']}")
+        logger.info(f"  aggregation: {self.extrema_weighting['aggregation']}")
+        logger.info(
+            f"  aggregation_normalization: {self.extrema_weighting['aggregation_normalization']}"
+        )
+        logger.info(f"  standardization: {self.extrema_weighting['standardization']}")
+        logger.info(
+            f"  robust_quantiles: ({format_number(self.extrema_weighting['robust_quantiles'][0])}, {format_number(self.extrema_weighting['robust_quantiles'][1])})"
+        )
+        logger.info(
+            f"  mmad_scaling_factor: {format_number(self.extrema_weighting['mmad_scaling_factor'])}"
+        )
+        logger.info(f"  normalization: {self.extrema_weighting['normalization']}")
+        logger.info(
+            f"  minmax_range: ({format_number(self.extrema_weighting['minmax_range'][0])}, {format_number(self.extrema_weighting['minmax_range'][1])})"
+        )
+        logger.info(
+            f"  sigmoid_scale: {format_number(self.extrema_weighting['sigmoid_scale'])}"
+        )
+        logger.info(
+            f"  softmax_temperature: {format_number(self.extrema_weighting['softmax_temperature'])}"
+        )
+        logger.info(f"  rank_method: {self.extrema_weighting['rank_method']}")
+        logger.info(f"  gamma: {format_number(self.extrema_weighting['gamma'])}")
+
+        logger.info("Extrema Smoothing:")
+        logger.info(f"  method: {self.extrema_smoothing['method']}")
+        logger.info(f"  window: {self.extrema_smoothing['window']}")
+        logger.info(f"  beta: {format_number(self.extrema_smoothing['beta'])}")
+        logger.info(f"  polyorder: {self.extrema_smoothing['polyorder']}")
+        logger.info(f"  mode: {self.extrema_smoothing['mode']}")
+        logger.info(
+            f"  bandwidth: {format_number(self.extrema_smoothing['bandwidth'])}"
+        )
+
+        logger.info("Reversal Confirmation:")
+        logger.info(f"  lookback_period: {self._reversal_lookback_period}")
+        logger.info(f"  decay_ratio: {format_number(self._reversal_decay_ratio)}")
+        logger.info(
+            f"  min_natr_ratio_percent: {format_number(self._reversal_min_natr_ratio_percent)}"
+        )
+        logger.info(
+            f"  max_natr_ratio_percent: {format_number(self._reversal_max_natr_ratio_percent)}"
+        )
+
+        exit_pricing = self.config.get("exit_pricing", {})
+        trade_price_target = exit_pricing.get("trade_price_target", "moving_average")
+        logger.info("Exit Pricing:")
+        logger.info(f"  trade_price_target: {trade_price_target}")
+        logger.info(f"  thresholds_calibration: {self._exit_thresholds_calibration}")
+
+        logger.info("Custom Stoploss:")
+        logger.info(
+            f"  natr_ratio_percent: {format_number(QuickAdapterV3.CUSTOM_STOPLOSS_NATR_RATIO_PERCENT)}"
+        )
+
+        logger.info("Partial Exit Stages:")
+        for stage, (
+            natr_ratio_percent,
+            stake_percent,
+        ) 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)}"
+            )
+
+        logger.info("Protections:")
+        if self.protections:
+            for protection in self.protections:
+                method = protection.get("method", "Unknown")
+                logger.info(f"  {method}:")
+                for key, value in protection.items():
+                    if key != "method":
+                        if isinstance(value, (int, float)):
+                            logger.info(f"    {key}: {format_number(value)}")
+                        else:
+                            logger.info(f"    {key}: {value}")
+        else:
+            logger.info("  No protections enabled")
+
+        logger.info("=" * 60)
+
     @staticmethod
     def _df_signature(df: DataFrame) -> DfSignature:
         n = len(df)
@@ -416,14 +512,6 @@ class QuickAdapterV3(IStrategy):
         self._reversal_min_natr_ratio_percent = float(min_natr_ratio_percent)
         self._reversal_max_natr_ratio_percent = float(max_natr_ratio_percent)
 
-        logger.debug(
-            "reversal_confirmation: lookback_period=%s, decay_ratio=%s, natr_ratio_percent_range=(%s, %s)",
-            self._reversal_lookback_period,
-            format_number(self._reversal_decay_ratio),
-            format_number(self._reversal_min_natr_ratio_percent),
-            format_number(self._reversal_max_natr_ratio_percent),
-        )
-
     def feature_engineering_expand_all(
         self, dataframe: DataFrame, period: int, metadata: dict[str, Any], **kwargs
     ) -> DataFrame:
@@ -1384,7 +1472,9 @@ class QuickAdapterV3(IStrategy):
         if df.empty:
             return None
 
-        stoploss_distance = self.get_stoploss_distance(df, trade, current_rate, 0.7860)
+        stoploss_distance = self.get_stoploss_distance(
+            df, trade, current_rate, QuickAdapterV3.CUSTOM_STOPLOSS_NATR_RATIO_PERCENT
+        )
         if isna(stoploss_distance) or stoploss_distance <= 0:
             return None
         return stoploss_from_absolute(
index 2be4aab11441479bd9cdb353e15ca734c76063d7..289a04922e706a9a4a5b25502a61827eb115309b 100644 (file)
@@ -146,6 +146,8 @@ DEFAULTS_EXTREMA_WEIGHTING: Final[dict[str, Any]] = {
 
 DEFAULT_EXTREMA_WEIGHT: Final[float] = 1.0
 
+DEFAULT_FIT_LIVE_PREDICTIONS_CANDLES: Final[int] = 100
+
 
 def get_distance(p1: T, p2: T) -> T:
     return abs(p1 - p2)