]> Piment Noir Git Repositories - freqai-strategies.git/commitdiff
feat(qav3): add kmedoids metric to MO trial selection
authorJérôme Benoit <jerome.benoit@piment-noir.org>
Tue, 23 Sep 2025 21:28:41 +0000 (23:28 +0200)
committerJérôme Benoit <jerome.benoit@piment-noir.org>
Tue, 23 Sep 2025 21:28:41 +0000 (23:28 +0200)
Signed-off-by: Jérôme Benoit <jerome.benoit@piment-noir.org>
quickadapter/docker-compose.yml
quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py
quickadapter/user_data/strategies/QuickAdapterV3.py

index ff5e80bfd40ad17d95375bbc9dcfc058cd9868e9..cee493a6a53bf40c48cfc30a8f2b0433ff578662 100644 (file)
@@ -19,10 +19,12 @@ services:
         FROM freqtradeorg/freqtrade:stable_freqai
 
         ARG optuna_version
+        ARG scikit_learn_extra_version
         ARG skimage_version
-        RUN pip install --user optuna==$${optuna_version} optuna-integration==$${optuna_version} optuna-dashboard scikit-image==$${skimage_version}
+        RUN pip install --user optuna==$${optuna_version} optuna-integration==$${optuna_version} optuna-dashboard scikit-learn-extra==$${scikit_learn_extra_version} scikit-image==$${skimage_version}
       args:
         - optuna_version=4.5.0
+        - scikit_learn_extra_version=0.3.0
         - skimage_version=0.25.2
     restart: unless-stopped
     container_name: freqtrade-quickadapter
index 8d2c468a6c83b494588e65b82648471fea2fb40f..9ce1183cb604cc76fb7b6fe8fb3d3be3bfa36352 100644 (file)
@@ -17,7 +17,7 @@ import sklearn
 from freqtrade.freqai.base_models.BaseRegressionModel import BaseRegressionModel
 from freqtrade.freqai.data_kitchen import FreqaiDataKitchen
 from numpy.typing import NDArray
-
+from sklearn_extra.cluster import KMedoids
 from Utils import (
     calculate_min_extrema,
     calculate_n_extrema,
@@ -60,7 +60,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
     https://github.com/sponsors/robcaulk
     """
 
-    version = "3.7.112"
+    version = "3.7.113"
 
     @cached_property
     def _optuna_config(self) -> dict[str, Any]:
@@ -795,6 +795,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
             "weighted_sum",
             "kmeans",
             "kmeans2",
+            "kmedoids",
             "knn_power_mean",
             "knn_percentile",
             "knn_min",
@@ -984,11 +985,11 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
                 label_kmeans_selection = self.ft_params.get(
                     "label_kmeans_selection", "min"
                 )
-                ordered_cluster_labels = np.argsort(cluster_center_distances_to_ideal)
+                ordered_cluster_indices = np.argsort(cluster_center_distances_to_ideal)
                 trial_distances = np.full(n_samples, np.inf)
                 best_cluster_indices = None
-                for cluster_label in ordered_cluster_labels:
-                    cluster_indices = np.flatnonzero(cluster_labels == cluster_label)
+                for cluster_index in ordered_cluster_indices:
+                    cluster_indices = np.flatnonzero(cluster_labels == cluster_index)
                     if cluster_indices.size:
                         best_cluster_indices = cluster_indices
                         break
@@ -1021,6 +1022,86 @@ class QuickAdapterRegressorV3(BaseRegressionModel):
                             f"Unsupported label_kmeans_selection: {label_kmeans_selection}. Supported are medoid/min"
                         )
                 return trial_distances
+            elif metric == "kmedoids":
+                if n_samples == 1:
+                    return np.array([0.0])
+                if n_samples < 2:
+                    return np.full(n_samples, np.inf)
+                n_clusters = min(max(2, int(np.sqrt(n_samples / 2))), 10, n_samples)
+                label_kmedoids_metric = self.ft_params.get(
+                    "label_kmedoids_metric", "euclidean"
+                )
+                if label_kmedoids_metric in {
+                    "mahalanobis",
+                    "seuclidean",
+                    "jensenshannon",
+                }:
+                    raise ValueError(
+                        f"Unsupported label_kmedoids_metric: {label_kmedoids_metric}. Supported are euclidean/minkowski/cityblock/chebyshev/..."
+                    )
+                kmedoids_kwargs: dict[str, Any] = {
+                    "metric": label_kmedoids_metric,
+                    "random_state": 42,
+                    "init": "k-medoids++",
+                    "method": "pam",
+                }
+                kmedoids = KMedoids(n_clusters=n_clusters, **kmedoids_kwargs)
+                cluster_labels = kmedoids.fit_predict(normalized_matrix)
+                medoid_indices = kmedoids.medoid_indices_
+                cdist_kwargs: dict[str, Any] = {}
+                if label_kmedoids_metric == "minkowski":
+                    cdist_kwargs["p"] = (
+                        label_p_order if label_p_order is not None else 2.0
+                    )
+                medoid_distances_to_ideal = sp.spatial.distance.cdist(
+                    normalized_matrix[medoid_indices],
+                    ideal_point_2d,
+                    metric=label_kmedoids_metric,
+                    **cdist_kwargs,
+                ).flatten()
+                ordered_medoid_indices = medoid_indices[
+                    np.argsort(medoid_distances_to_ideal)
+                ]
+                label_kmedoids_selection = self.ft_params.get(
+                    "label_kmedoids_selection", "min"
+                )
+                trial_distances = np.full(n_samples, np.inf)
+                best_cluster_indices = None
+                for medoid_index in ordered_medoid_indices:
+                    cluster_index = cluster_labels[medoid_index]
+                    cluster_indices = np.flatnonzero(cluster_labels == cluster_index)
+                    if cluster_indices.size:
+                        best_cluster_indices = cluster_indices
+                        break
+                if best_cluster_indices is not None and best_cluster_indices.size > 0:
+                    if label_kmedoids_selection == "medoid":
+                        best_cluster_matrix = normalized_matrix[best_cluster_indices]
+                        trial_distances[best_cluster_indices] = (
+                            self._pairwise_distance_sums(
+                                best_cluster_matrix,
+                                label_kmedoids_metric,
+                                p=(
+                                    label_p_order
+                                    if label_kmedoids_metric == "minkowski"
+                                    and label_p_order is not None
+                                    else None
+                                ),
+                            )
+                        )
+                    elif label_kmedoids_selection == "min":
+                        trial_distances[best_cluster_indices] = (
+                            sp.spatial.distance.cdist(
+                                normalized_matrix[best_cluster_indices],
+                                ideal_point_2d,
+                                metric=label_kmedoids_metric,
+                                **cdist_kwargs,
+                            ).flatten()
+                        )
+                    else:
+                        raise ValueError(
+                            f"Unsupported label_kmedoids_selection: {label_kmedoids_selection}. Supported are medoid/min"
+                        )
+                return trial_distances
             elif metric in {"knn_power_mean", "knn_percentile", "knn_min", "knn_max"}:
                 if n_samples < 2:
                     return np.full(n_samples, np.inf)
index 6d326fb1ed3762a685221c8856db5685c1f347b5..ba3a8cf128c9c86f8b4b71386fabf7dd63b55c71 100644 (file)
@@ -65,7 +65,7 @@ class QuickAdapterV3(IStrategy):
     INTERFACE_VERSION = 3
 
     def version(self) -> str:
-        return "3.3.156"
+        return "3.3.157"
 
     timeframe = "5m"