{"record":{"id":"552114b68de73ab1","repo":"HKUDS/Vibe-Trading","slug":"a-label-cannot-end-before-the-observation-it-belon","errorCode":null,"errorMessage":"a label cannot end before the observation it belongs to starts","messagePattern":"a label cannot end before the observation it belongs to starts","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"agent/src/quantlib/crossvalidation.py","lineNumber":152,"sourceCode":"        starts = label_end_times.index\n        ends = label_end_times.to_numpy()\n        # searchsorted on the start index converts label end *times* into label\n        # end *positions*; the right insertion point minus one keeps a label\n        # that ends between two observations attached to the earlier one.\n        positions = np.searchsorted(starts, ends, side=\"right\") - 1\n        positions = np.clip(positions, np.arange(len(starts)), len(starts) - 1)\n        span_ends = positions.astype(int)\n    else:\n        span_ends = np.asarray(label_end_times, dtype=float)\n        if span_ends.ndim != 1:\n            raise ValueError(f\"label_end_times must be 1-D, got shape {span_ends.shape}\")\n        if span_ends.size == 0:\n            raise ValueError(\"label_end_times is empty\")\n        if not np.isfinite(span_ends).all():\n            raise ValueError(\"label_end_times holds a non-finite value\")\n        span_ends = span_ends.astype(int)\n        if (span_ends < np.arange(span_ends.size)).any():\n            raise ValueError(\n                \"a label cannot end before the observation it belongs to starts\"\n            )\n\n    if n_samples is not None and span_ends.size != n_samples:\n        raise ValueError(\n            f\"label_end_times has {span_ends.size} entries but the sample has {n_samples}\"\n        )\n    return span_ends\n\n\ndef _apply_purge_and_embargo(\n    label_ends: np.ndarray,\n    test_mask: np.ndarray,\n    embargo_size: int,\n) -> tuple[np.ndarray, int, int]:\n    \"\"\"Build a training mask that is purged of overlap and embargoed after.\n\n    Args:","sourceCodeStart":134,"sourceCodeEnd":170,"githubUrl":"https://github.com/HKUDS/Vibe-Trading/blob/80ffdda44c5c4db0dd84d70e051cca591cea67df/agent/src/quantlib/crossvalidation.py#L134-L170","documentation":"Raised by _as_label_spans in the purged cross-validation module when a label's end time is smaller than the index of the observation it belongs to, i.e. a label span ends before the observation's own start time. Since observation i is assumed to start at time i, label_end_times[i] < i is temporally impossible for forward-looking labels. The library rejects it because purging/embargo logic assumes labels extend at least to their observation start.","triggerScenarios":"Calling purged_kfold_splits, purged_walk_forward_splits, combinatorial_purged_splits, or detect_boundary_leakage with a label_end_times array (integer or datetime-converted-to-int) where some entry is less than its positional index, e.g. label_end_times=[5, 0, 7].","commonSituations":"Misaligned label arrays after slicing/dropping rows without reindexing, using label start times instead of end times, off-by-one when converting timestamps to integer indices, or sorting the sample without sorting label_end_times alongside.","solutions":["Verify label_end_times[i] >= i for every row; regenerate labels from the actual horizon end dates","If labels are stored as timestamps, convert them with the same time-origin/index mapping used for the observations","Check for row drops/reordering: recompute label_end_times on the same dataframe you pass as the sample","Pass label_end_times=None to fall back to the identity span np.arange(n_samples) while debugging"],"exampleFix":"// before\nlabel_ends = df['label_start'].to_numpy()  # starts, not ends\nsplits = list(purged_kfold_splits(X, n_folds=5, label_end_times=label_ends))\n// after\nlabel_ends = df['label_end'].to_numpy()\nassert (label_ends >= np.arange(len(label_ends))).all()\nsplits = list(purged_kfold_splits(X, n_folds=5, label_end_times=label_ends))","handlingStrategy":"validation","validationCode":"import numpy as np\nlabel_ends = np.asarray(label_end_times)\nassert label_ends.size == len(X)\nassert np.isfinite(label_ends.astype(float)).all()\nassert (label_ends >= np.arange(label_ends.size)).all(), 'label ends before observation start'","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always derive label_end_times on the same dataframe you pass as the sample","Use label end timestamps, not starts","Assert monotone-consistent spans before splitting"],"tags":["crossvalidation","data-validation","label-leakage","quantlib"],"backgroundTag":"invalid-label-time-ranges","analyzedSha":"80ffdda44c5c4db0dd84d70e051cca591cea67df","analyzedAt":"2026-08-28T12:46:38.989Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}