From 6da29070d951562eaf82f080fceeff23899c7a6c Mon Sep 17 00:00:00 2001 From: =?utf8?q?J=C3=A9r=C3=B4me=20Benoit?= Date: Thu, 30 Jul 2026 19:23:46 +0200 Subject: [PATCH] refactor(quickadapter): consolidate causal epsilon-fill idioms (#171) (#177) * refactor(quickadapter): consolidate causal epsilon-fill idioms (#171) Extract the duplicated "segment-ends" idiom `np.flatnonzero(np.r_[a[1:] != a[:-1], True])` into a pure helper `_segment_ends` and call it from its three sites: `_causal_impute_weights`, `_compute_causal_epsilon_fill`, and `_compute_knn_pivot_sigma_availability` (where the flipped operand order is equivalent by symmetry of `!=`). Route the two verbatim-duplicated `fill_epsilon_baseline` `ValueError` messages in `_compute_epsilon_floor` and `_compute_causal_epsilon_fill` through the existing `enum_error_message` factory; the produced string is unchanged and the scalar vs expanding baseline computations are untouched. Purely internal deduplication; no behavior change. Bit-for-bit equivalence of the four affected functions verified against the prior revision by randomized fuzzing in the quickadapter-freqtrade container. * docs(quickadapter): clarify _segment_ends run contiguity (#171) --- quickadapter/user_data/strategies/Utils.py | 25 ++++++++++++---------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/quickadapter/user_data/strategies/Utils.py b/quickadapter/user_data/strategies/Utils.py index e0e47f4..7e72c19 100644 --- a/quickadapter/user_data/strategies/Utils.py +++ b/quickadapter/user_data/strategies/Utils.py @@ -2204,6 +2204,11 @@ def _impute_weights( return weights +def _segment_ends(a: NDArray[np.integer]) -> NDArray[np.intp]: + """Indices of the last element of each consecutive run of equal values in ``a``.""" + return np.flatnonzero(np.r_[a[1:] != a[:-1], True]) + + def _causal_impute_weights( weights: NDArray[np.floating], *, @@ -2223,7 +2228,7 @@ def _causal_impute_weights( .fillna(default_weight) .to_numpy(dtype=float) ) - event_ends = np.flatnonzero(np.r_[availability[1:] != availability[:-1], True]) + event_ends = _segment_ends(availability) event_medians = np.repeat( running_median[event_ends], np.diff(np.r_[-1, event_ends]), @@ -2729,8 +2734,9 @@ def _compute_epsilon_floor( b = float(np.nanmedian(pivot_values)) else: raise ValueError( - f"Invalid fill_epsilon_baseline value {baseline!r}: " - f"supported values are {', '.join(FILL_EPSILON_BASELINES)}" + enum_error_message( + "fill_epsilon_baseline", baseline, FILL_EPSILON_BASELINES + ) ) if not np.isfinite(b): b = 0.0 @@ -2829,13 +2835,12 @@ def _compute_causal_epsilon_fill( ) else: raise ValueError( - f"Invalid fill_epsilon_baseline value {baseline!r}: " - f"supported values are {', '.join(FILL_EPSILON_BASELINES)}" + enum_error_message( + "fill_epsilon_baseline", baseline, FILL_EPSILON_BASELINES + ) ) - event_ends = np.flatnonzero( - np.r_[pivot_available_at[1:] != pivot_available_at[:-1], True] - ) + event_ends = _segment_ends(pivot_available_at) availability_events = pivot_available_at[event_ends] event_floors = float(label_weighting["fill_epsilon"]) * running_baseline[event_ends] @@ -3069,9 +3074,7 @@ def _compute_knn_pivot_sigma_availability( pivot_positions = pivot_indices.astype(np.int64, copy=False) pivot_confirmations = known_at_positions[pivot_positions] - confirmation_group_ends = np.flatnonzero( - np.r_[pivot_confirmations[:-1] != pivot_confirmations[1:], True] - ) + confirmation_group_ends = _segment_ends(pivot_confirmations) pivot_spacing = _ZIGZAG_MIN_CONFIRMATION_SLOPES + 1 last_future_pivot_position = n - pivot_spacing kth_distances = ( -- 2.53.0