HKUDS/Vibe-Trading · error · ValueError

cash-flow kind {flow.kind!r} on {flow.date} is classified ne

Error message

cash-flow kind {flow.kind!r} on {flow.date} is classified neither external nor internal, so it cannot be treated as either. Pass external_kinds=[...] or internal_kinds=[...] to say which side of the portfolio boundary it crosses. Known external: {', '.join(sorted(external))}. Known internal: {', '.join(sorted(internal))}.

What it means

Every non-valuation cash flow must be classified as external or internal before return math can treat it. This error means a flow's kind is in neither the external nor the internal set, and the library refuses to silently guess which side of the portfolio boundary it crosses, because guessing wrong would corrupt the return.

Source

Thrown at agent/src/quantlib/performance.py:413

        DEFAULT_INTERNAL_KINDS
        if internal_kinds is None
        else frozenset(normalize_kind(kind) for kind in internal_kinds)
    )
    overlap = external & internal
    if overlap:
        raise ValueError(
            "a kind cannot be both external and internal; overlapping: "
            f"{', '.join(sorted(overlap))}"
        )

    selected: list[tuple[date, float]] = []
    for flow in flows:
        if flow.is_valuation:
            continue
        if flow.kind in external:
            selected.append((flow.date, -flow.amount))
        elif flow.kind not in internal:
            raise ValueError(
                f"cash-flow kind {flow.kind!r} on {flow.date} is classified "
                "neither external nor internal, so it cannot be treated as "
                "either. Pass external_kinds=[...] or internal_kinds=[...] to "
                "say which side of the portfolio boundary it crosses. Known "
                f"external: {', '.join(sorted(external))}. Known internal: "
                f"{', '.join(sorted(internal))}."
            )
    selected.sort(key=lambda item: item[0])
    return tuple(selected)


def _annualize(total_return: float, days: int) -> float | None:
    """Annualise a holding-period return, or decline to when the window is short.

    Args:
        total_return: Return over the whole window, as a decimal fraction.
        days: Calendar days spanned by the window.

View on GitHub (pinned to 80ffdda44c)

Solutions

  1. Pass external_kinds=[...] or internal_kinds=[...] including the unknown kind shown in the message, choosing the side of the boundary it actually crosses.
  2. Fix the kind string at the source (typo, casing, whitespace) so it matches a known kind.
  3. Extend your ingestion layer with a whitelist validation so unclassified kinds are caught at import time, not during analytics.

Example fix

# before
time_weighted_return(valuations=marks, flows=flows)  # flows contain kind='wire_in' -> ValueError

# after
time_weighted_return(
    valuations=marks,
    flows=flows,
    external_kinds=['contribution', 'withdrawal', 'wire_in'],
)
Defensive patterns

Strategy: validation

Validate before calling

KNOWN = DEFAULT_EXTERNAL_KINDS | DEFAULT_INTERNAL_KINDS | set(external_kinds or ()) | set(internal_kinds or ())
unknown = [f for f in flows if not f.is_valuation and normalize_kind(f.kind) not in KNOWN]
assert not unknown, f'unclassified kinds: {[(f.kind, f.date) for f in unknown]}'

Type guard

def flows_fully_classified(flows, external, internal) -> bool:
    return all(f.is_valuation or normalize_kind(f.kind) in (external | internal) for f in flows)

Try / catch

try:
    r = time_weighted_return(valuations=marks, flows=flows)
except ValueError as e:
    if 'neither external nor internal' in str(e):
        r = time_weighted_return(valuations=marks, flows=flows,
                                 external_kinds=[*default_ext, 'wire_in'])
    else:
        raise

Prevention

When it happens

Trigger: Calling time_weighted_return / modified_dietz_return / money_weighted_return / external_flows with a flow whose kind string (after normalization, e.g. lowercase) is not in DEFAULT_EXTERNAL_KINDS, DEFAULT_INTERNAL_KINDS, or the sets you passed explicitly — e.g. kind='crypto_transfer' or a typo like 'contribution '.

Common situations: A new cash-flow kind introduced by an upstream system; typos or casing/whitespace differences in kind strings; a data migration that renamed kinds ('deposit' -> 'client_deposit'); version upgrades that changed the default kind sets.

Related errors


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