HKUDS/Vibe-Trading · error · ValueError

tier {self.name!r} does not conserve cash: lp {self.lp_amoun

Error message

tier {self.name!r} does not conserve cash: lp {self.lp_amount!r} + gp {self.gp_amount!r} != {self.amount!r}

What it means

A waterfall tier must conserve cash: lp_amount + gp_amount must equal amount within _ALLOCATION_TOLERANCE. This invariant catches split computations that leak or create dollars between the LP and GP sleeves.

Source

Thrown at agent/src/quantlib/fundmath.py:1035

                if any figure is negative. A waterfall that does not conserve
                cash is a bug, never a rounding artefact worth tolerating.
        """
        for label, value in (
            ("amount", self.amount),
            ("lp_amount", self.lp_amount),
            ("gp_amount", self.gp_amount),
        ):
            if value < -_ALLOCATION_TOLERANCE:
                raise ValueError(
                    f"tier {self.name!r} has a negative {label} of {value!r}"
                )
        if not math.isclose(
            self.lp_amount + self.gp_amount,
            self.amount,
            rel_tol=_ALLOCATION_TOLERANCE,
            abs_tol=_ALLOCATION_TOLERANCE,
        ):
            raise ValueError(
                f"tier {self.name!r} does not conserve cash: lp {self.lp_amount!r} "
                f"+ gp {self.gp_amount!r} != {self.amount!r}"
            )


@dataclass(frozen=True)
class WaterfallResult:
    """Outcome of a European whole-of-fund distribution waterfall.

    Attributes:
        distributable: Cash entering the waterfall.
        contributed_capital: LP capital that must be returned first.
        preferred_amount: Preferred return owed before the GP earns anything.
        tiers: The tiers in payment order.
        lp_total: Everything allocated to the limited partners.
        gp_total: Everything allocated to the general partner.
        unreturned_capital: Contributed capital still outstanding, positive when
            the distributable amount did not cover the return of capital.

View on GitHub (pinned to 80ffdda44c)

Solutions

  1. Derive one sleeve from the other: gp_amount = round(amount - lp_amount, 2)
  2. Round amount and both sleeves consistently to the same precision
  3. Recompute the tier from a single split rate (gp_amount = amount * carry_rate)

Example fix

# before
tier = Tier(name="carry", amount=100.0, lp_amount=80.0, gp_amount=15.0)

# after
amount = 100.0
lp = round(amount * 0.8, 2)
tier = Tier(name="carry", amount=amount, lp_amount=lp, gp_amount=round(amount - lp, 2))
Defensive patterns

Strategy: validation

Validate before calling

import math
assert math.isclose(lp + gp, amount, rel_tol=1e-9, abs_tol=1e-9), (lp, gp, amount)

Prevention

When it happens

Trigger: Constructing a tier with amount=100.0, lp_amount=80.0, gp_amount=15.0 (sums to 95, not 100), typically from independent rounding of lp/gp shares.

Common situations: Rounding each sleeve to cents separately; carry split computed from a rate applied to a different base than amount; assembling tiers from separate data sources.

Related errors


AI-assisted analysis of HKUDS/Vibe-Trading@80ffdda44c (2026-08-28). Data as JSON: /api/errors/e36bdd7282e2fa99. Report an issue: GitHub.