From: Jérôme Benoit Date: Mon, 5 Jan 2026 20:41:43 +0000 (+0100) Subject: feat(quickadapter): add multiple aggregation methods for combined extrema weighting... X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=8b1905cd37d388a42322baa28ae29c63a9cd7c89;p=freqai-strategies.git feat(quickadapter): add multiple aggregation methods for combined extrema weighting (v3.10.4) - Add 5 new aggregation methods: arithmetic_mean, harmonic_mean, quadratic_mean, weighted_median, softmax - Replace weighted_average (deprecated) with arithmetic_mean as new default - Add softmax_temperature parameter (default: 1.0) for softmax aggregation - Implement all methods using scipy.stats.pmean for power means (p=1,-1,2) and numpy for weighted_median - Add softmax aggregation with temperature scaling and coefficient weighting - Add validation and logging for softmax_temperature parameter - Update README with precise mathematical formulas for all aggregation methods - Bump version to 3.10.4 in strategy and model - Add conditional logging for softmax_temperature when aggregation is softmax --- diff --git a/README.md b/README.md index c445b7c..20560ad 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,8 @@ docker compose up -d --build | _Extrema weighting_ | | | | | freqai.extrema_weighting.strategy | `none` | enum {`none`,`amplitude`,`amplitude_threshold_ratio`,`volume_rate`,`speed`,`efficiency_ratio`,`volume_weighted_efficiency_ratio`,`combined`} | Extrema weighting metric: none (`none`), swing amplitude (`amplitude`), swing amplitude / median volatility-threshold ratio (`amplitude_threshold_ratio`), swing volume per candle (`volume_rate`), swing speed (`speed`), swing efficiency ratio (`efficiency_ratio`), swing volume-weighted efficiency ratio (`volume_weighted_efficiency_ratio`), or combined metrics aggregation (`combined`). Switching between `none` and any other strategy requires deleting trained models. | | freqai.extrema_weighting.metric_coefficients | {} | dict[str, float] | Per-metric coefficients for `combined` strategy. Keys: `amplitude`, `amplitude_threshold_ratio`, `volume_rate`, `speed`, `efficiency_ratio`, `volume_weighted_efficiency_ratio`. | -| freqai.extrema_weighting.aggregation | `weighted_average` | enum {`weighted_average`,`geometric_mean`} | Metric aggregation method for `combined` strategy. `weighted_average`=Σ(coef·metric)/Σ(coef), `geometric_mean`=∏(metric^coef)^(1/Σcoef). | +| freqai.extrema_weighting.aggregation | `arithmetic_mean` | enum {`arithmetic_mean`,`geometric_mean`,`harmonic_mean`,`quadratic_mean`,`weighted_median`,`softmax`} | Metric aggregation method for `combined` strategy. `arithmetic_mean`=(Σ(w·m)/Σ(w)), `geometric_mean`=(∏(m^w))^(1/Σw), `harmonic_mean`=Σ(w)/(Σ(w/m)), `quadratic_mean`=(Σ(w·m²)/Σ(w))^(1/2), `weighted_median`=Q₀.₅(m,w), `softmax`=Σ(m·s_i) where s_i=w_i·exp(m_i/T)/Σ(w_j·exp(m_j/T)). | +| freqai.extrema_weighting.softmax_temperature | 1.0 | float > 0 | Temperature T for `softmax` aggregation, controls distribution sharpness. | | freqai.extrema_weighting.standardization | `none` | enum {`none`,`zscore`,`robust`,`mmad`,`power_yj`} | Standardization method applied to smoothed weighted extrema before normalization. `none`=w, `zscore`=(w-μ)/σ, `robust`=(w-median)/IQR, `mmad`=(w-median)/(MAD·k), `power_yj`=YJ(w). | | freqai.extrema_weighting.robust_quantiles | [0.25, 0.75] | list[float] where 0 <= Q1 < Q3 <= 1 | Quantile range for robust standardization, Q1 and Q3. | | freqai.extrema_weighting.mmad_scaling_factor | 1.4826 | float > 0 | Scaling factor for MMAD standardization. | diff --git a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py index e69fe5f..2c9aadf 100644 --- a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py +++ b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py @@ -87,7 +87,7 @@ class QuickAdapterRegressorV3(BaseRegressionModel): https://github.com/sponsors/robcaulk """ - version = "3.10.3" + version = "3.10.4" _TEST_SIZE: Final[float] = 0.1 diff --git a/quickadapter/user_data/strategies/ExtremaWeightingTransformer.py b/quickadapter/user_data/strategies/ExtremaWeightingTransformer.py index 695d7b4..613472f 100644 --- a/quickadapter/user_data/strategies/ExtremaWeightingTransformer.py +++ b/quickadapter/user_data/strategies/ExtremaWeightingTransformer.py @@ -44,10 +44,21 @@ COMBINED_METRICS: Final[tuple[CombinedMetric, ...]] = ( "volume_weighted_efficiency_ratio", ) -CombinedAggregation = Literal["weighted_average", "geometric_mean"] +CombinedAggregation = Literal[ + "arithmetic_mean", + "geometric_mean", + "harmonic_mean", + "quadratic_mean", + "weighted_median", + "softmax", +] COMBINED_AGGREGATIONS: Final[tuple[CombinedAggregation, ...]] = ( - "weighted_average", + "arithmetic_mean", "geometric_mean", + "harmonic_mean", + "quadratic_mean", + "weighted_median", + "softmax", ) WEIGHT_STRATEGIES: Final[tuple[WeightStrategy, ...]] = ( @@ -81,7 +92,8 @@ NORMALIZATION_TYPES: Final[tuple[NormalizationType, ...]] = ( DEFAULTS_EXTREMA_WEIGHTING: Final[dict[str, Any]] = { "strategy": WEIGHT_STRATEGIES[0], # "none" "metric_coefficients": {}, - "aggregation": COMBINED_AGGREGATIONS[0], # "weighted_average" + "aggregation": COMBINED_AGGREGATIONS[0], # "arithmetic_mean" + "softmax_temperature": 1.0, # Phase 1: Standardization "standardization": STANDARDIZATION_TYPES[0], # "none" "robust_quantiles": (0.25, 0.75), diff --git a/quickadapter/user_data/strategies/QuickAdapterV3.py b/quickadapter/user_data/strategies/QuickAdapterV3.py index 373af19..1065d30 100644 --- a/quickadapter/user_data/strategies/QuickAdapterV3.py +++ b/quickadapter/user_data/strategies/QuickAdapterV3.py @@ -57,6 +57,7 @@ from Utils import ( zigzag, zlema, ) +from ExtremaWeightingTransformer import COMBINED_AGGREGATIONS TradeDirection = Literal["long", "short"] InterpolationDirection = Literal["direct", "inverse"] @@ -106,7 +107,7 @@ class QuickAdapterV3(IStrategy): _PLOT_EXTREMA_MIN_EPS: Final[float] = 0.01 def version(self) -> str: - return "3.10.3" + return "3.10.4" timeframe = "5m" timeframe_minutes = timeframe_to_minutes(timeframe) @@ -564,6 +565,12 @@ class QuickAdapterV3(IStrategy): f" sigmoid_scale: {format_number(self.extrema_weighting['sigmoid_scale'])}" ) logger.info(f" gamma: {format_number(self.extrema_weighting['gamma'])}") + if ( + self.extrema_weighting["aggregation"] == COMBINED_AGGREGATIONS[5] + ): # "softmax" + logger.info( + f" softmax_temperature: {format_number(self.extrema_weighting['softmax_temperature'])}" + ) logger.info("Extrema Smoothing:") logger.info(f" method: {self.extrema_smoothing['method']}") diff --git a/quickadapter/user_data/strategies/Utils.py b/quickadapter/user_data/strategies/Utils.py index 51d904f..dfc27c8 100644 --- a/quickadapter/user_data/strategies/Utils.py +++ b/quickadapter/user_data/strategies/Utils.py @@ -136,6 +136,19 @@ def get_extrema_weighting_config( ) aggregation = DEFAULTS_EXTREMA_WEIGHTING["aggregation"] + softmax_temperature = extrema_weighting.get( + "softmax_temperature", DEFAULTS_EXTREMA_WEIGHTING["softmax_temperature"] + ) + if ( + not isinstance(softmax_temperature, (int, float)) + or not np.isfinite(softmax_temperature) + or softmax_temperature <= 0 + ): + logger.warning( + f"Invalid extrema_weighting softmax_temperature value {softmax_temperature!r}: must be a finite number > 0, using default {DEFAULTS_EXTREMA_WEIGHTING['softmax_temperature']!r}" + ) + softmax_temperature = DEFAULTS_EXTREMA_WEIGHTING["softmax_temperature"] + # Phase 1: Standardization standardization = extrema_weighting.get( "standardization", DEFAULTS_EXTREMA_WEIGHTING["standardization"] @@ -250,6 +263,7 @@ def get_extrema_weighting_config( "strategy": strategy, "metric_coefficients": metric_coefficients, "aggregation": aggregation, + "softmax_temperature": softmax_temperature, # Phase 1: Standardization "standardization": standardization, "robust_quantiles": robust_quantiles, @@ -521,14 +535,36 @@ def _aggregate_metrics( stacked_metrics: NDArray[np.floating], coefficients: NDArray[np.floating], aggregation: CombinedAggregation, + softmax_temperature: float, ) -> NDArray[np.floating]: - if aggregation == COMBINED_AGGREGATIONS[0]: # "weighted_average" - return np.average(stacked_metrics, axis=0, weights=coefficients) + if aggregation == COMBINED_AGGREGATIONS[0]: # "arithmetic_mean" + return sp.stats.pmean(stacked_metrics.T, p=1.0, weights=coefficients, axis=1) elif aggregation == COMBINED_AGGREGATIONS[1]: # "geometric_mean" - return np.asarray( - sp.stats.gmean(stacked_metrics.T, weights=coefficients, axis=1), - dtype=float, + return sp.stats.pmean(stacked_metrics.T, p=0.0, weights=coefficients, axis=1) + elif aggregation == COMBINED_AGGREGATIONS[2]: # "harmonic_mean" + return sp.stats.pmean(stacked_metrics.T, p=-1.0, weights=coefficients, axis=1) + elif aggregation == COMBINED_AGGREGATIONS[3]: # "quadratic_mean" + return sp.stats.pmean(stacked_metrics.T, p=2.0, weights=coefficients, axis=1) + elif aggregation == COMBINED_AGGREGATIONS[4]: # "weighted_median" + return np.array( + [ + np.quantile( + stacked_metrics[:, i], + 0.5, + weights=coefficients, + method="inverted_cdf", + ) + for i in range(stacked_metrics.shape[1]) + ] ) + elif aggregation == COMBINED_AGGREGATIONS[5]: # "softmax" + scaled_metrics = stacked_metrics / softmax_temperature + softmax_weights = sp.special.softmax(scaled_metrics, axis=0) + combined_weights = softmax_weights * coefficients[:, np.newaxis] + combined_weights = combined_weights / np.sum( + combined_weights, axis=0, keepdims=True + ) + return np.sum(stacked_metrics * combined_weights, axis=0) else: raise ValueError( f"Invalid aggregation value {aggregation!r}: supported values are {', '.join(COMBINED_AGGREGATIONS)}" @@ -545,6 +581,7 @@ def _compute_combined_weights( volume_weighted_efficiency_ratios: list[float], metric_coefficients: dict[str, Any], aggregation: CombinedAggregation, + softmax_temperature: float, ) -> NDArray[np.floating]: if len(indices) == 0: return np.asarray([], dtype=float) @@ -585,7 +622,9 @@ def _compute_combined_weights( stacked_metrics = np.vstack(imputed_metrics) coefficients_array = np.asarray(coefficients_list, dtype=float) - return _aggregate_metrics(stacked_metrics, coefficients_array, aggregation) + return _aggregate_metrics( + stacked_metrics, coefficients_array, aggregation, softmax_temperature + ) def compute_extrema_weights( @@ -630,6 +669,7 @@ def compute_extrema_weights( volume_weighted_efficiency_ratios=volume_weighted_efficiency_ratios, metric_coefficients=extrema_weighting["metric_coefficients"], aggregation=extrema_weighting["aggregation"], + softmax_temperature=extrema_weighting["softmax_temperature"], ) else: