{"record":{"id":"e8057c1621a1e748","repo":"HKUDS/Vibe-Trading","slug":"ts-rank-window-must-be-1-got-n","errorCode":null,"errorMessage":"ts_rank window must be >= 1, got {n}","messagePattern":"ts_rank window must be >= 1, got (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"agent/src/factors/base.py","lineNumber":104,"sourceCode":"    \"\"\"\n    df = _as_float(df)\n    abs_sum = df.abs().sum(axis=1, skipna=True)\n    abs_sum = abs_sum.where(abs_sum > 0)  # zero → NaN\n    return df.mul(a).div(abs_sum, axis=0)\n\n\ndef ts_rank(df: pd.DataFrame, n: int) -> pd.DataFrame:\n    \"\"\"Rolling rank (last value's rank within the n-window), per column.\n\n    Warmup (first ``n-1`` rows per column) returns NaN. Result is a percentile\n    in [0, 1] so it is compositionally compatible with cross-sectional rank.\n\n    Uses numpy ``sliding_window_view`` for vectorized computation (~45x faster\n    than pandas rolling().apply()). Note: ``bottleneck.move_rank`` computes\n    Spearman rank correlation, not percentile rank, so it is not used here.\n    \"\"\"\n    if n < 1:\n        raise ValueError(f\"ts_rank window must be >= 1, got {n}\")\n\n    def _last_rank(arr: np.ndarray) -> float:\n        if np.isnan(arr).all():\n            return np.nan\n        last = arr[-1]\n        if np.isnan(last):\n            return np.nan\n        valid = arr[~np.isnan(arr)]\n        if valid.size == 0:\n            return np.nan\n        # average rank for ties; pct\n        less = (valid < last).sum()\n        eq = (valid == last).sum()\n        rank_avg = less + 0.5 * (eq + 1)\n        return float(rank_avg / valid.size)\n\n    arr = df.to_numpy(dtype=np.float64)\n    T, C = arr.shape","sourceCodeStart":86,"sourceCodeEnd":122,"githubUrl":"https://github.com/HKUDS/Vibe-Trading/blob/80ffdda44c5c4db0dd84d70e051cca591cea67df/agent/src/factors/base.py#L86-L122","documentation":"ts_rank validates its rolling window n and requires n >= 1; smaller values (0 or negatives) raise immediately. A window of 1 is meaningful here (the single value is its own rank). The function uses numpy sliding_window_view for speed, with warmup positions returning NaN.","triggerScenarios":"ts_rank(df, 0), ts_rank(df, -5), or a computed window that evaluates to 0, e.g. n = len(df) - lookback with lookback == len(df). Called from compute() in factor pipelines and directly in tests.","commonSituations":"Config-driven window sizes where a parameter is unset and defaults to 0; dynamic windows derived from data length that underflow on short series; passing a percentage (0.05) instead of a count.","solutions":["Pass a positive integer window (>= 1)","Validate/clamp computed windows: n = max(1, n) only if that is semantically correct, otherwise raise early with context","Check config values for window parameters before running the factor pipeline"],"exampleFix":"# before\nout = ts_rank(df, n=0)\n\n# after\nout = ts_rank(df, n=20)","handlingStrategy":"validation","validationCode":"if not isinstance(n, int) or n < 1:\n    raise ValueError(f'invalid ts_rank window: {n!r}')\nout = ts_rank(df, n)","typeGuard":"def is_valid_ts_rank_window(n) -> bool:\n    return isinstance(n, int) and n >= 1","tryCatchPattern":null,"preventionTips":["Validate all window configs once at startup","Floor auto-scaled windows to the operator minimum (with an explicit policy)","Boundary-test operators with n at and below the minimum"],"tags":["python","factors","rolling-window","parameter-validation"],"backgroundTag":"rolling-window-invalid","analyzedSha":"80ffdda44c5c4db0dd84d70e051cca591cea67df","analyzedAt":"2026-08-28T12:46:38.989Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}