From 290fd31e2f7322aa516b79b82eac27716aaf2fd7 Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Wed, 31 Dec 2025 01:01:40 +0100 Subject: [PATCH] feat(quickadapter): add TOPSIS metric for multi-objective HPO trial selection MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Signed-off-by: Jérôme Benoit --- README.md | 13 +++-- .../reward_space_analysis.py | 2 +- .../freqaimodels/QuickAdapterRegressorV3.py | 58 ++++++++++++++++++- .../user_data/strategies/QuickAdapterV3.py | 2 +- 4 files changed, 66 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 11138ad..8c9b3be 100644 --- a/README.md +++ b/README.md @@ -88,16 +88,17 @@ docker compose up -d --build | freqai.feature_parameters.min_label_natr_multiplier | 9.0 | float > 0 | Minimum labeling NATR multiplier used for reversals labeling HPO. (Deprecated alias: `freqai.feature_parameters.min_label_natr_ratio`) | | freqai.feature_parameters.max_label_natr_multiplier | 12.0 | float > 0 | Maximum labeling NATR multiplier used for reversals labeling HPO. (Deprecated alias: `freqai.feature_parameters.max_label_natr_ratio`) | | freqai.feature_parameters.label_frequency_candles | `auto` | int >= 2 \| `auto` | Reversals labeling frequency. `auto` = max(2, 2 \* number of whitelisted pairs). | -| freqai.feature_parameters.label_metric | `euclidean` | string (supported: `euclidean`,`minkowski`,`cityblock`,`chebyshev`,`mahalanobis`,`seuclidean`,`jensenshannon`,`sqeuclidean`,...) | Metric used in distance calculations to ideal point. | +| freqai.feature_parameters.label_metric | `euclidean` | string | Metric for Pareto front trial selection (SciPy distance metrics or selection metrics like `topsis`, `medoid`, `kmeans`, `kmedoids`, ...). | | freqai.feature_parameters.label_weights | [1/7,1/7,1/7,1/7,1/7,1/7,1/7] | list[float] | Per-objective weights used in distance calculations to ideal point. Objectives: (1) number of detected reversals, (2) median swing amplitude, (3) median (swing amplitude / median volatility-threshold ratio), (4) median swing volume per candle, (5) median swing speed, (6) median swing efficiency ratio, (7) median swing volume-weighted efficiency ratio. | -| freqai.feature_parameters.label_p_order | `None` | float \| None | p-order used by `minkowski` / `power_mean` (optional). | -| freqai.feature_parameters.label_medoid_metric | `euclidean` | string | Metric used with `medoid`. | -| freqai.feature_parameters.label_kmeans_metric | `euclidean` | string | Metric used for k-means clustering. | +| freqai.feature_parameters.label_p_order | `None` | float \| None | p-order for Minkowski distance. Used by `minkowski`, `power_mean`, `medoid`, `kmeans`, `kmedoids`, `knn`, `topsis` when their sub-metric is `minkowski`. | +| freqai.feature_parameters.label_medoid_metric | `euclidean` | string | Distance metric used with `medoid`. | +| freqai.feature_parameters.label_kmeans_metric | `euclidean` | string | Distance metric used for k-means clustering. | | freqai.feature_parameters.label_kmeans_selection | `min` | enum {`min`,`medoid`} | Strategy to select trial in the best k-means cluster. | -| freqai.feature_parameters.label_kmedoids_metric | `euclidean` | string | Metric used for k-medoids clustering. | +| freqai.feature_parameters.label_kmedoids_metric | `euclidean` | string | Distance metric used for k-medoids clustering. | | freqai.feature_parameters.label_kmedoids_selection | `min` | enum {`min`,`medoid`} | Strategy to select trial in the best k-medoids cluster. | +| freqai.feature_parameters.label_topsis_metric | `euclidean` | string | Distance metric for TOPSIS ideal/anti-ideal point calculations. | | freqai.feature_parameters.label_knn_metric | `minkowski` | string | Distance metric for KNN. | -| 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_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). | | freqai.feature_parameters.label_knn_n_neighbors | 5 | int >= 1 | Number of neighbors for KNN. | | _Predictions extrema_ | | | | | 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. | diff --git a/ReforceXY/reward_space_analysis/reward_space_analysis.py b/ReforceXY/reward_space_analysis/reward_space_analysis.py index 7a4e537..bef1074 100644 --- a/ReforceXY/reward_space_analysis/reward_space_analysis.py +++ b/ReforceXY/reward_space_analysis/reward_space_analysis.py @@ -3250,7 +3250,7 @@ def compute_pbrs_components( ---------------------- R'(s,a,s') = R(s,a,s') + Δ(s,a,s') - Non Canonical PBRS Formula + Non-Canonical PBRS Formula -------------------------- R'(s,a,s') = R(s,a,s') + Δ(s,a,s') + entry_additive + exit_additive diff --git a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py index 7c05c64..949382e 100644 --- a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py +++ b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py @@ -72,7 +72,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): https://github.com/sponsors/robcaulk """ - version = "3.8.4" + version = "3.8.5" _TEST_SIZE: Final[float] = 0.1 @@ -170,6 +170,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): "knn_min", "knn_max", "medoid", + "topsis", ) _METRICS: Final[tuple[str, ...]] = ( @@ -685,6 +686,18 @@ class QuickAdapterRegressorV3(BaseRegressionModel): label_p_order_reason = ( f"{label_metric} (via label_knn_metric={label_knn_metric})" ) + elif ( + label_metric == QuickAdapterRegressorV3._CUSTOM_METRICS[17] + ): # "topsis" + label_topsis_metric = self.ft_params.get( + "label_topsis_metric", + QuickAdapterRegressorV3._SCIPY_METRICS[2], # "euclidean" default + ) + if ( + label_topsis_metric == QuickAdapterRegressorV3._SCIPY_METRICS[5] + ): # "minkowski" + label_p_order_is_used = True + label_p_order_reason = f"{label_metric} (via label_topsis_metric={label_topsis_metric})" if label_p_order_config is not None: logger.info( @@ -808,6 +821,16 @@ class QuickAdapterRegressorV3(BaseRegressionModel): f" label_knn_p_order: {format_number(label_knn_p_order_default)} (default for {label_metric})" ) + label_topsis_metric_config = self.ft_params.get("label_topsis_metric") + if label_topsis_metric_config is not None: + logger.info(f" label_topsis_metric: {label_topsis_metric_config}") + elif ( + label_metric == QuickAdapterRegressorV3._CUSTOM_METRICS[17] + ): # "topsis" + logger.info( + f" label_topsis_metric: {QuickAdapterRegressorV3._SCIPY_METRICS[2]} (default for {label_metric})" + ) + logger.info("Predictions Extrema Configuration:") predictions_extrema = self.predictions_extrema logger.info( @@ -2191,6 +2214,39 @@ class QuickAdapterRegressorV3(BaseRegressionModel): return np.nanmin(neighbor_distances, axis=1) elif metric == QuickAdapterRegressorV3._CUSTOM_METRICS[15]: # "knn_max" return np.nanmax(neighbor_distances, axis=1) + elif metric == QuickAdapterRegressorV3._CUSTOM_METRICS[17]: # "topsis" + # TOPSIS (Hwang & Yoon, 1981): returns D+ / (D+ + D-) for argmin selection + # where D+ = distance to ideal [1,1,...], D- = distance to anti-ideal [0,0,...] + label_topsis_metric = self.ft_params.get( + "label_topsis_metric", + QuickAdapterRegressorV3._SCIPY_METRICS[2], # "euclidean" + ) + cdist_kwargs: dict[str, Any] = { + "metric": label_topsis_metric, + "w": np_weights, + } + if ( + label_topsis_metric + == QuickAdapterRegressorV3._SCIPY_METRICS[5] # "minkowski" + ): + cdist_kwargs["p"] = ( + label_p_order + if label_p_order is not None and np.isfinite(label_p_order) + else self._get_label_p_order_default(label_topsis_metric) + ) + dist_to_ideal = sp.spatial.distance.cdist( + normalized_matrix, ideal_point_2d, **cdist_kwargs + ).flatten() + dist_to_anti_ideal = sp.spatial.distance.cdist( + normalized_matrix, np.zeros((1, n_objectives)), **cdist_kwargs + ).flatten() + + denominator = dist_to_ideal + dist_to_anti_ideal + zero_mask = np.isclose(denominator, 0.0) + denominator[zero_mask] = 1.0 + topsis_score = dist_to_ideal / denominator + topsis_score[zero_mask] = 0.5 + return topsis_score else: raise ValueError( f"Invalid label metric {metric!r}. Supported: {', '.join(metrics)}" diff --git a/quickadapter/user_data/strategies/QuickAdapterV3.py b/quickadapter/user_data/strategies/QuickAdapterV3.py index 00fa93c..1b3a467 100644 --- a/quickadapter/user_data/strategies/QuickAdapterV3.py +++ b/quickadapter/user_data/strategies/QuickAdapterV3.py @@ -106,7 +106,7 @@ class QuickAdapterV3(IStrategy): _TRADING_MODES: Final[tuple[TradingMode, ...]] = ("spot", "margin", "futures") def version(self) -> str: - return "3.8.4" + return "3.8.5" timeframe = "5m" -- 2.43.0