crucible / engine /quantities.py
s-ttp's picture
SPGI hardening: trigger magnitude check, implicit comparisons, token-structure rules
f9517e4 verified
Raw History Blame Contribute Delete
7.08 kB
"""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"