From bb32276332cca4d7ce474608dbffae68366c8497 Mon Sep 17 00:00:00 2001 From: TPTBusiness Date: Sun, 26 Apr 2026 18:59:58 +0200 Subject: [PATCH] fix(auto-fixer): disable _fix_min_periods for intraday data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fixer was raising min_periods to match window size, which causes all-NaN output for intraday factors with 96 bars/day — window=240 means zero valid bars per day, window=60 means 61% NaN per day. Critics were consistently flagging this as incorrect for intraday factors. The LLM now controls its own min_periods. Co-Authored-By: Claude Sonnet 4.6 --- .../components/coder/factor_coder/auto_fixer.py | 8 +++++--- test/qlib/test_auto_fixer.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/rdagent/components/coder/factor_coder/auto_fixer.py b/rdagent/components/coder/factor_coder/auto_fixer.py index 75ea23e1..4352bf17 100644 --- a/rdagent/components/coder/factor_coder/auto_fixer.py +++ b/rdagent/components/coder/factor_coder/auto_fixer.py @@ -51,15 +51,17 @@ class FactorAutoFixer: self.fixes_applied = [] fixed_code = code - # Apply fixes in order - groupby fixes MUST come before min_periods fixes + # Apply fixes in order + # NOTE: _fix_min_periods is intentionally excluded — it increased min_periods to + # match window size, which causes all-NaN output for intraday data with 96 bars/day + # (window=240 > 96 means zero valid bars per day). The LLM sets its own min_periods. fix_methods = [ self._fix_reset_index_groupby, # First: fix groupby(level=N) after reset_index() self._fix_groupby_mixed_levels, # Second: fix groupby(level=[int, str]) self._fix_groupby_column_on_multiindex, # Third: fix groupby(['instrument','date']) on MultiIndex self._fix_chained_groupby, # Fourth: fix groupby(level=N).groupby('date') chain self._fix_rolling_ddof, # Fifth: remove unsupported ddof kwarg - self._fix_groupby_apply_to_transform, # Fifth: fix groupby patterns - self._fix_min_periods, # Sixth: fix min_periods in rolling calls + self._fix_groupby_apply_to_transform, # Sixth: fix groupby patterns self._fix_inf_nan_handling, # Seventh: add inf/nan handling self._fix_data_range_processing, # Eighth: ensure full data range self._fix_multiindex_groupby, # Ninth: ensure groupby on MultiIndex diff --git a/test/qlib/test_auto_fixer.py b/test/qlib/test_auto_fixer.py index 4bc83878..fb7defd9 100644 --- a/test/qlib/test_auto_fixer.py +++ b/test/qlib/test_auto_fixer.py @@ -73,6 +73,20 @@ class TestChainedGroupby: assert '.groupby("date")' not in result +class TestMinPeriodsNotTouched: + def test_small_min_periods_preserved(self, fixer): + # _fix_min_periods is disabled — LLM-set min_periods must not be changed. + # window=60, min_periods=1 should stay as-is (was wrongly raised to 60 before). + result = fixer.fix("df.groupby(level=1)['x'].transform(lambda x: x.rolling(window=60, min_periods=1).mean())") + assert "min_periods=1" in result + + def test_large_window_min_periods_preserved(self, fixer): + # window=240 > 96 bars/day: if min_periods were set to 240 the output would be + # all-NaN for intraday data. Verify we leave it untouched. + result = fixer.fix("df['x'] = df.groupby(level=1)['y'].transform(lambda x: x.rolling(240, min_periods=10).std())") + assert "min_periods=10" in result + + class TestRollingDdof: def test_removes_ddof_from_rolling_args(self, fixer): result = fixer.fix("df.rolling(20, min_periods=1, ddof=1).std()")