From 528f02ea5eb1035098d8349ff3a2452e03ca1a6a Mon Sep 17 00:00:00 2001 From: uditDewan Date: Sun, 5 Jul 2026 00:17:56 -0400 Subject: [PATCH] fix(ops): make PairRolling input assert actually check its inputs The assert in PairRolling._load_internal was vacuous: it evaluated any([isinstance(feature_left, Expression), feature_right, Expression]), where the bare Expression class object is always truthy, so the check could never fail. Two non-Expression inputs slipped through and later died with AttributeError ("'float' object has no attribute 'rolling'"). Check isinstance on both sides so invalid inputs fail fast with the intended message. --- qlib/data/ops.py | 4 +-- tests/ops/test_pair_rolling_inputs.py | 50 +++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 tests/ops/test_pair_rolling_inputs.py diff --git a/qlib/data/ops.py b/qlib/data/ops.py index d9a2ffbb3e3..920df45448f 100644 --- a/qlib/data/ops.py +++ b/qlib/data/ops.py @@ -1413,8 +1413,8 @@ def __str__(self): return "{}({},{},{})".format(type(self).__name__, self.feature_left, self.feature_right, self.N) def _load_internal(self, instrument, start_index, end_index, *args): - assert any( - [isinstance(self.feature_left, Expression), self.feature_right, Expression] + assert isinstance(self.feature_left, Expression) or isinstance( + self.feature_right, Expression ), "at least one of two inputs is Expression instance" if isinstance(self.feature_left, Expression): diff --git a/tests/ops/test_pair_rolling_inputs.py b/tests/ops/test_pair_rolling_inputs.py new file mode 100644 index 00000000000..605fc85bea6 --- /dev/null +++ b/tests/ops/test_pair_rolling_inputs.py @@ -0,0 +1,50 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import unittest + +import numpy as np +import pandas as pd + +from qlib.data.base import Expression +from qlib.data.ops import Cov + + +class StubFeature(Expression): + """Minimal expression returning a fixed series, so operators can be tested without a data provider.""" + + def __init__(self, values, name): + self._values = list(values) + self._name = name + + def __str__(self): + # unique per stub: Expression.load caches series keyed on str(self) + return self._name + + def _load_internal(self, instrument, start_index, end_index, *args): + return pd.Series(self._values, dtype=float) + + def get_longest_back_rolling(self): + return 0 + + def get_extended_window_size(self): + return 0, 0 + + +class TestPairRollingInputCheck(unittest.TestCase): + """PairRolling must reject two non-Expression inputs instead of failing later with AttributeError.""" + + def test_two_numeric_inputs_rejected(self): + with self.assertRaises(AssertionError): + Cov(1.0, 2.0, 3).load("inst", 0, 4, "day") + + def test_expression_inputs_accepted(self): + left = [1.0, 2.0, 3.0, 4.0, 5.0] + right = [2.0, 1.0, 4.0, 3.0, 6.0] + res = Cov(StubFeature(left, "cov_l"), StubFeature(right, "cov_r"), 3).load("inst", 0, 4, "day") + expected = pd.Series(left).rolling(3, min_periods=1).cov(pd.Series(right)) + np.testing.assert_allclose(res.values, expected.values) + + +if __name__ == "__main__": + unittest.main()