From 83a4a2495d00af64ff0dcf6a1960b7513634c085 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Tue, 23 Sep 2025 23:28:41 +0200 Subject: [PATCH] feat(qav3): add kmedoids metric to MO trial selection MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Signed-off-by: Jérôme Benoit --- quickadapter/docker-compose.yml | 4 +- .../freqaimodels/QuickAdapterRegressorV3.py | 91 ++++++++++++++++++- .../user_data/strategies/QuickAdapterV3.py | 2 +- 3 files changed, 90 insertions(+), 7 deletions(-) diff --git a/quickadapter/docker-compose.yml b/quickadapter/docker-compose.yml index ff5e80b..cee493a 100644 --- a/quickadapter/docker-compose.yml +++ b/quickadapter/docker-compose.yml @@ -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 diff --git a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py index 8d2c468..9ce1183 100644 --- a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py +++ b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py @@ -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) diff --git a/quickadapter/user_data/strategies/QuickAdapterV3.py b/quickadapter/user_data/strategies/QuickAdapterV3.py index 6d326fb..ba3a8cf 100644 --- a/quickadapter/user_data/strategies/QuickAdapterV3.py +++ b/quickadapter/user_data/strategies/QuickAdapterV3.py @@ -65,7 +65,7 @@ class QuickAdapterV3(IStrategy): INTERFACE_VERSION = 3 def version(self) -> str: - return "3.3.156" + return "3.3.157" timeframe = "5m" -- 2.43.0