From: Jérôme Benoit Date: Mon, 22 Jun 2026 01:54:59 +0000 (+0200) Subject: fix(quickadapter): route reversed train weights through support_policy (#98) X-Git-Url: https://git.piment-noir.org/?a=commitdiff_plain;h=63429c5fe8b23550d20e1c7b1216f9c4be2b89f6;p=freqai-strategies.git fix(quickadapter): route reversed train weights through support_policy (#98) PR #85 added `_compose_train_weights_with_support` (gates training-set weights through `support_policy`) and `_compose_eval_weights` (eval-side, deliberately bypasses `support_policy`). The `reverse_train_test_order` path in `_make_train_test_split_datasets` and `_make_timeseries_split_datasets` swapped slices AT THE FINAL `build_data_dictionary` call -- AFTER weight composition -- so the actual-train slot received weights composed by `_compose_eval_weights` (silent bypass), and the actual-test slot received weights composed by `_compose_train_weights_with_support` (wrong direction, typically a no-op under `support_policy='fallback'` default). Reachable only under `causal_mode=false` (deprecated; acausal baselines only) since `causal_mode=true` rejects `reverse_train_test_order=true` upfront. Fix: perform the train/test slice swap BEFORE weight composition so the `train_*` and `test_*` identifiers map to their actual training roles throughout. Both call sites converge to a single `dk.build_data_dictionary` return; context strings in `support_policy` log/raise messages now reflect the true train/test role. Add an upfront `ValueError` in `_make_train_test_split_datasets` when `test_size=0` AND `reverse_train_test_order=True`, mirroring the existing `causal_mode`/reverse rejection pattern. The `timeseries_split` path already rejects `test_size < 1` upstream of the swap. Behavior change in the deprecated path: `support_policy='raise'` now correctly raises on actual-train insufficient support; `support_policy='fallback'` now correctly warns. Reviewed by three parallel Oracle passes (math + algorithmics + scope/reachability; Python state-of-the-art + harmonization + implementation elegance; documentation + terminology + completeness) at design stage and again post-implementation, each citing upstream evidence from `freqtrade/freqai/`. Follow-up from PR #80 review, deferred during PR #90. Closes #92. --- diff --git a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py index fc40831..4188140 100644 --- a/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py +++ b/quickadapter/user_data/freqaimodels/QuickAdapterRegressorV3.py @@ -1883,6 +1883,12 @@ class QuickAdapterRegressorV3(BaseRegressionModel): f"Invalid data_split_parameters.test_size value {test_size!r}: " f"must be int or float" ) + if test_size == 0 and feat_dict.get("reverse_train_test_order", False): + raise ValueError( + "data_split_parameters.test_size=0 is incompatible with " + "feature_parameters.reverse_train_test_order=True: the empty " + "test slice cannot be promoted to the training slot" + ) if test_size != 0: if weights.label is None: @@ -1983,6 +1989,19 @@ class QuickAdapterRegressorV3(BaseRegressionModel): ) ) + if feat_dict.get("reverse_train_test_order", False): + ( + train_features, test_features, + train_labels, test_labels, + train_base_weights, test_base_weights, + train_label_weights, test_label_weights, + ) = ( + test_features, train_features, + test_labels, train_labels, + test_base_weights, train_base_weights, + test_label_weights, train_label_weights, + ) + train_weights = QuickAdapterRegressorV3._compose_train_weights_with_support( train_base_weights, train_label_weights, @@ -1998,15 +2017,6 @@ class QuickAdapterRegressorV3(BaseRegressionModel): else: test_weights = test_base_weights - if feat_dict.get("reverse_train_test_order", False): - return dk.build_data_dictionary( - test_features, - train_features, - test_labels, - train_labels, - test_weights, - train_weights, - ) return dk.build_data_dictionary( train_features, test_features, @@ -2336,11 +2346,6 @@ class QuickAdapterRegressorV3(BaseRegressionModel): None if weights.label is None else weights.label[train_idx] ) test_label_weights = None if weights.label is None else weights.label[test_idx] - test_weights = QuickAdapterRegressorV3._compose_eval_weights( - test_base_weights, - test_label_weights, - context=f"[{dk.pair}] timeseries_split:test", - ) if causal_mode: row_positions = QuickAdapterRegressorV3._row_positions( @@ -2373,22 +2378,31 @@ class QuickAdapterRegressorV3(BaseRegressionModel): else: _log_known_at_none_once(dk.pair, "timeseries_split causal guard") + if feat_dict.get("reverse_train_test_order", False): + ( + train_features, test_features, + train_labels, test_labels, + train_base_weights, test_base_weights, + train_label_weights, test_label_weights, + ) = ( + test_features, train_features, + test_labels, train_labels, + test_base_weights, train_base_weights, + test_label_weights, train_label_weights, + ) + train_weights = QuickAdapterRegressorV3._compose_train_weights_with_support( train_base_weights, train_label_weights, weights.label_weighting_config, context=f"[{dk.pair}] timeseries_split:train", ) + test_weights = QuickAdapterRegressorV3._compose_eval_weights( + test_base_weights, + test_label_weights, + context=f"[{dk.pair}] timeseries_split:test", + ) - if feat_dict.get("reverse_train_test_order", False): - return dk.build_data_dictionary( - test_features, - train_features, - test_labels, - train_labels, - test_weights, - train_weights, - ) return dk.build_data_dictionary( train_features, test_features,