"""Typed units and quantity types: what a number IS, not only how large it is. A rate stored as a decimal (0.0335) can be a margin LEVEL of 3.35%, a margin CHANGE of 335 basis points or a growth rate of 3.35%. The decimal is the same, the quantity is not. Every rate-like figure therefore carries: * ``unit`` — the unit it was stated in: PERCENT, PERCENTAGE_POINTS, BASIS_POINTS, DECIMAL_FRACTION, MULTIPLE, COUNT, USD, USD_MILLION, USD_BILLION, USD_PER_SHARE; * ``quantity_type`` — what it measures: LEVEL, CHANGE, GROWTH_RATE, SPREAD, MARGIN_LEVEL, MARGIN_CHANGE, YIELD, PROBABILITY. Canonical values: rates, changes and spreads as decimals (335 bps → 0.0335, 3.35 pp → 0.0335, 3.35% → 0.0335); money in USD. The canonical number may coincide; the unit and quantity type never collapse, and formatting is driven by them, never inferred from the size of the stored number. """ from __future__ import annotations # ── units ───────────────────────────────────────────────────────────────────────────── DECIMAL_FRACTION = "DECIMAL_FRACTION" PERCENT = "PERCENT" PERCENTAGE_POINTS = "PERCENTAGE_POINTS" BASIS_POINTS = "BASIS_POINTS" MULTIPLE = "MULTIPLE" COUNT = "COUNT" USD = "USD" USD_MILLION = "USD_MILLION" USD_BILLION = "USD_BILLION" USD_PER_SHARE = "USD_PER_SHARE" UNITS = (DECIMAL_FRACTION, PERCENT, PERCENTAGE_POINTS, BASIS_POINTS, MULTIPLE, COUNT, USD, USD_MILLION, USD_BILLION, USD_PER_SHARE) RATE_UNITS = (DECIMAL_FRACTION, PERCENT, PERCENTAGE_POINTS, BASIS_POINTS) MONEY_UNITS = (USD, USD_MILLION, USD_BILLION, USD_PER_SHARE) # stated value × factor = canonical value (decimal for rates, USD for money) TO_CANONICAL = {DECIMAL_FRACTION: 1.0, PERCENT: 0.01, PERCENTAGE_POINTS: 0.01, BASIS_POINTS: 0.0001, MULTIPLE: 1.0, COUNT: 1.0, USD: 1.0, USD_MILLION: 1e6, USD_BILLION: 1e9, USD_PER_SHARE: 1.0} # ── quantity types ──────────────────────────────────────────────────────────────────── LEVEL = "LEVEL" CHANGE = "CHANGE" GROWTH_RATE = "GROWTH_RATE" SPREAD = "SPREAD" MARGIN_LEVEL = "MARGIN_LEVEL" MARGIN_CHANGE = "MARGIN_CHANGE" YIELD = "YIELD" PROBABILITY = "PROBABILITY" QUANTITY_TYPES = (LEVEL, CHANGE, GROWTH_RATE, SPREAD, MARGIN_LEVEL, MARGIN_CHANGE, YIELD, PROBABILITY) CHANGE_TYPES = (CHANGE, MARGIN_CHANGE, SPREAD) # units a quantity type may be stated in ALLOWED_UNITS = { LEVEL: MONEY_UNITS + (COUNT, MULTIPLE), GROWTH_RATE: (PERCENT, DECIMAL_FRACTION), MARGIN_LEVEL: (PERCENT, DECIMAL_FRACTION), MARGIN_CHANGE: (BASIS_POINTS, PERCENTAGE_POINTS), CHANGE: (BASIS_POINTS, PERCENTAGE_POINTS) + MONEY_UNITS, SPREAD: (BASIS_POINTS, PERCENTAGE_POINTS), YIELD: (PERCENT, DECIMAL_FRACTION), PROBABILITY: (PERCENT, DECIMAL_FRACTION), } # economic concepts for guidance and claims: a change or a growth rate is never stored under a level concept CONCEPT_QUANTITY = { "REVENUE": LEVEL, "REVENUE_GROWTH": GROWTH_RATE, "ORGANIC_REVENUE_GROWTH": GROWTH_RATE, "OPERATING_MARGIN": MARGIN_LEVEL, "OPERATING_MARGIN_CHANGE": MARGIN_CHANGE, "NET_MARGIN": MARGIN_LEVEL, "GROSS_MARGIN": MARGIN_LEVEL, "GROSS_MARGIN_CHANGE": MARGIN_CHANGE, "EPS": LEVEL, "EPS_GROWTH": GROWTH_RATE, "OPERATING_INCOME": LEVEL, "OPERATING_EXPENSES": LEVEL, "INTEREST_EXPENSE": LEVEL, "AMORTIZATION": LEVEL, "TAX_RATE": MARGIN_LEVEL, "FREE_CASH_FLOW": LEVEL, "CAPEX": LEVEL, "SHARE_REPURCHASE": LEVEL, "DIVIDEND": LEVEL, "NET_INTEREST_INCOME": LEVEL, "NET_INTEREST_MARGIN": MARGIN_LEVEL, "OTHER": None, } class QuantityError(ValueError): """A unit or quantity type that does not describe the figure.""" def to_canonical(value, unit): if value is None: return None if unit not in TO_CANONICAL: raise QuantityError(f"unknown unit {unit!r}") return float(value) * TO_CANONICAL[unit] def check(unit, quantity_type): """(ok, reason): is this unit a valid way to state this quantity type?""" if unit not in UNITS: return False, f"unknown unit {unit!r}" if quantity_type not in QUANTITY_TYPES: return False, f"unknown quantity type {quantity_type!r}" if unit not in ALLOWED_UNITS[quantity_type]: return False, f"{quantity_type} cannot be stated in {unit}" return True, "" def _num(x, dec): s = f"{x:,.{dec}f}" return s.rstrip("0").rstrip(".") if dec and "." in s else s def format_value(value, unit, quantity_type=None, *, signed=False): """One canonical value, formatted from its unit and quantity type (never from its magnitude).""" if value is None: return None v = float(value) sg = "+" if (signed and v > 0) else "" if unit == BASIS_POINTS: return f"{sg}{_num(v * 10000.0, 0)} bps" if unit == PERCENTAGE_POINTS: return f"{sg}{_num(v * 100.0, 2)} percentage points" if unit in (PERCENT, DECIMAL_FRACTION): return f"{sg}{v * 100.0:,.1f}%" if unit == MULTIPLE: return f"{v:,.1f}x" if unit == COUNT: return f"{int(round(v)):,}" if unit == USD_PER_SHARE: return f"{sg}${v:,.2f}" if unit in (USD, USD_MILLION, USD_BILLION): a = abs(v) s = "-" if v < 0 else sg if a >= 1e12: return f"{s}${a / 1e12:,.2f}T" if a >= 1e9: return f"{s}${a / 1e9:,.2f}B" if a >= 1e6: return f"{s}${a / 1e6:,.0f}M" if a >= 1e8 else f"{s}${a / 1e6:,.1f}M" return f"{s}${a:,.0f}" raise QuantityError(f"cannot format unit {unit!r}") def format_range(low, high, unit, quantity_type=None): lo, hi = format_value(low, unit, quantity_type), format_value(high, unit, quantity_type) if lo is None and hi is None: return None if lo is None or hi is None or lo == hi: return lo or hi if unit == BASIS_POINTS: return f"{_num(float(low) * 10000.0, 0)}–{_num(float(high) * 10000.0, 0)} bps" if unit == PERCENTAGE_POINTS: return f"{_num(float(low) * 100.0, 2)}–{_num(float(high) * 100.0, 2)} percentage points" return f"{lo}–{hi}" MAGNITUDE_BAND = (0.2, 5.0) def magnitude_ok(value, low, high=None): """(ok, reason): is a threshold measured against a source of the same unit of the same order of magnitude? A threshold of 3.6 bps against guidance of 335–360 bps is the same number in the wrong unit, not a judgement.""" refs = [abs(float(x)) for x in (low, high) if x is not None and float(x) != 0.0] if value is None or not refs: return True, "" v = abs(float(value)) ratio = min((v / r for r in refs), key=lambda q: abs(q - 1.0) if q else float("inf")) if v else 0.0 lo, hi = MAGNITUDE_BAND if lo <= ratio <= hi: return True, "" return False, f"threshold is {ratio:.3g}× its source value (outside {lo}–{hi}×): a unit slip, not a judgement"