Description
Rsquare in qlib/data/ops.py computes R² via a Cython kernel as
num / sqrt(var_x * var_y). For a near-constant window var_y ≈ 0, so
floating-point cancellation yields inf or a spurious finite value instead of
NaN (a degenerate 0/0 regression).
Rsquare._load_internal guards against this by masking windows whose std is ≈0
to NaN — but only on the rolling (N != 0) branch:
def _load_internal(self, instrument, start_index, end_index, *args):
_series = self.feature.load(instrument, start_index, end_index, *args)
if self.N == 0:
series = pd.Series(expanding_rsquare(_series.values), index=_series.index)
# <-- no guard here
else:
series = pd.Series(rolling_rsquare(_series.values, self.N), index=_series.index)
series.loc[np.isclose(_series.rolling(self.N, min_periods=1).std(), 0, atol=2e-05)] = np.nan
return series
The expanding (N == 0) branch is unguarded, so Rsquare($feature, 0) returns
inf/garbage on near-constant windows. Because ops.py sets
np.seterr(invalid="ignore"), no warning is emitted — the bad values silently
propagate into features (e.g. Alpha158/Alpha360) and downstream models.
Reproduction
Near-constant series [100, 100, 100, 100.000001, 100, 100]:
expanding_rsquare (N==0 path): [nan, nan, nan, inf, 0.01717987, inf]
rolling_rsquare(4) after mask: [nan, nan, nan, nan, nan, nan]
The expanding path leaks inf and a spurious 0.0172; the rolling path is
correctly NaN.
Fix
Apply the same std≈0 → NaN mask on the expanding branch (using expanding std).
Slope/Resi are unaffected — they divide by the x-variance (index 1..N),
which is always well-conditioned; only Rsquare divides by the y-variance.
PR incoming.
Description
Rsquareinqlib/data/ops.pycomputes R² via a Cython kernel asnum / sqrt(var_x * var_y). For a near-constant windowvar_y ≈ 0, sofloating-point cancellation yields
infor a spurious finite value instead ofNaN(a degenerate 0/0 regression).Rsquare._load_internalguards against this by masking windows whose std is ≈0to
NaN— but only on the rolling (N != 0) branch:The expanding (
N == 0) branch is unguarded, soRsquare($feature, 0)returnsinf/garbage on near-constant windows. Becauseops.pysetsnp.seterr(invalid="ignore"), no warning is emitted — the bad values silentlypropagate into features (e.g. Alpha158/Alpha360) and downstream models.
Reproduction
Near-constant series
[100, 100, 100, 100.000001, 100, 100]:The expanding path leaks
infand a spurious0.0172; the rolling path iscorrectly
NaN.Fix
Apply the same std≈0 →
NaNmask on the expanding branch (using expanding std).Slope/Resiare unaffected — they divide by the x-variance (index1..N),which is always well-conditioned; only
Rsquaredivides by the y-variance.PR incoming.