{"record":{"id":"12b1061c73d61edd","repo":"HKUDS/Vibe-Trading","slug":"contribution-on-flow-date-is-after-as-of-measur","errorCode":null,"errorMessage":"contribution on {flow.date} is after as_of={measurement}; a preferred return cannot accrue backwards","messagePattern":"contribution on (.+?) is after as_of=(.+?); a preferred return cannot accrue backwards","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"agent/src/quantlib/fundmath.py","lineNumber":984,"sourceCode":"    if days_per_year <= 0:\n        raise ValueError(f\"days_per_year must be positive, got {days_per_year!r}\")\n\n    contributions = tuple(series.filter(kind=contribution_kinds))\n    if not contributions:\n        return 0.0\n\n    if as_of is None:\n        if series.is_empty:\n            raise ValueError(\"as_of is required for an empty series\")\n        measurement = max(series.dates())\n    else:\n        measurement = normalize_date(as_of, field_name=\"as_of\")\n\n    total = 0.0\n    for flow in contributions:\n        years = (measurement - flow.date).days / days_per_year\n        if years < 0.0:\n            raise ValueError(\n                f\"contribution on {flow.date} is after as_of={measurement}; a \"\n                \"preferred return cannot accrue backwards\"\n            )\n        principal = -flow.amount\n        if compounding == \"compound\":\n            total += principal * ((1.0 + rate) ** years - 1.0)\n        else:\n            total += principal * rate * years\n    return total\n\n\n@dataclass(frozen=True)\nclass WaterfallTier:\n    \"\"\"One tier of a distribution waterfall and how it split.\n\n    Attributes:\n        name: Tier label, one of the ``TIER_*`` constants.\n        amount: Dollars that flowed through this tier.","sourceCodeStart":966,"sourceCodeEnd":1002,"githubUrl":"https://github.com/HKUDS/Vibe-Trading/blob/80ffdda44c5c4db0dd84d70e051cca591cea67df/agent/src/quantlib/fundmath.py#L966-L1002","documentation":"Preferred return accrues forward from each contribution date to the measurement date; a contribution dated after as_of would imply negative accrual time, which the function refuses rather than returning a negative hurdle.","triggerScenarios":"Calling preferred_return_amount(series, rate=0.08, as_of=date(2024,1,1)) when the series contains a contribution dated 2024-06-30 — i.e. as_of predates a draw.","commonSituations":"Measuring 'as of' a quarter-end that precedes a later capital call; date parsing that swaps day/month producing future dates; stale as_of defaults.","solutions":["Move as_of to on or after the latest contribution date (e.g. the latest mark date via _default_as_of semantics)","Fix misparsed dates (day/month swap) in the contribution records"],"exampleFix":"# before\npreferred_return_amount(series, rate=0.08, as_of=\"2024-01-01\")  # draw on 2024-06-30\n\n# after\npreferred_return_amount(series, rate=0.08, as_of=\"2024-12-31\")","handlingStrategy":"validation","validationCode":"last_contrib = max(f.date for f in series.filter(kind=CONTRIB_KINDS))\nif as_of is None:\n    as_of = max(last_contrib, *series.dates())\nelse:\n    assert as_of >= last_contrib, f\"as_of {as_of} predates draw {last_contrib}\"","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Parse dates with explicit formats to avoid day/month swaps","Derive as_of from the data rather than hardcoding"],"tags":["fund-math","preferred-return","date-ordering","as-of"],"backgroundTag":"date-order-violation","analyzedSha":"80ffdda44c5c4db0dd84d70e051cca591cea67df","analyzedAt":"2026-08-28T12:46:38.989Z","schemaVersion":2},"datasetVersion":"2026-08-28T16:17:29.566Z"}