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
- Pass external_kinds=[...] or internal_kinds=[...] including the unknown kind shown in the message, choosing the side of the boundary it actually crosses.
- Fix the kind string at the source (typo, casing, whitespace) so it matches a known kind.
- 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
- Validate kind strings at the ingestion boundary against a whitelist.
- Fail CI tests when a new kind appears unclassified.
- Normalize casing/whitespace of kinds before analytics.
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
- two valuations share the date {earlier[0]}; a single day can
- a kind cannot be both external and internal; overlapping: {'
- cash flow on {when} lies outside the valuation window {first
- delay requires n >= 1 (lookahead ban)
- delay requires n >= 1 (lookahead ban)
AI-assisted analysis of HKUDS/Vibe-Trading@80ffdda44c (2026-08-28).
Data as JSON: /api/errors/5dc0c7be5c79124e.
Report an issue: GitHub.