crucible / engine /units.py
s-ttp's picture
Legacy cleanup: state mode is the only debate path; text gate, claim sweep, report exporter, metric categories and 29 unused modules removed; LLM stage override replaced by a deterministic GAAP-distortion test; unused dependencies dropped
33b81e9 verified
Raw History Blame Contribute Delete
11.3 kB
"""One canonical representation and one formatting path for every rate-like value.
The defect this module exists to make impossible: the same canonical number, a model-implied
12-month return of 0.2375, rendered as "+23.8%" on the cover and "+0.2%" in the synthesis
paragraph, because one call site multiplied by 100 before appending "%" and the other did not.
Nothing in the code declared which convention a field used, so each renderer guessed.
Rules
-----
* Every rate-like field declares its storage unit in ``FIELD_UNITS``. The unit is never inferred
from the variable name at a call site.
* A rate-like value is rendered only through ``format_percent`` / ``Pct``. Appending a literal
"%" to a bare float is forbidden and is caught by ``scan_source_for_adhoc_percent``.
* ``parse_percent(format_percent(x)) ≈ x`` for every storage unit, which is what lets the final
render audit read a displayed string back and compare it with the canonical value.
"""
from __future__ import annotations
import re
from dataclasses import dataclass
# ── storage units ─────────────────────────────────────────────────────────────────────
DECIMAL_FRACTION = "DECIMAL_FRACTION" # 0.2375 means 23.75%
PERCENT_NUMBER = "PERCENT_NUMBER" # 23.75 means 23.75%
BASIS_POINTS = "BASIS_POINTS" # 2375 means 23.75%
RATIO = "RATIO" # 1.35 means 1.35x — NOT a percentage
UNITS = (DECIMAL_FRACTION, PERCENT_NUMBER, BASIS_POINTS, RATIO)
_TO_DECIMAL = {DECIMAL_FRACTION: 1.0, PERCENT_NUMBER: 0.01, BASIS_POINTS: 0.0001}
class UnitError(ValueError):
"""A rate-like value was used without a declared, convertible unit."""
# ── the declared storage semantics of every rate-like field ───────────────────────────
# Adding a rate-like field without an entry here is a test failure, not a silent default.
FIELD_UNITS = {
# returns
"expected_return": DECIMAL_FRACTION,
"low_estimate": DECIMAL_FRACTION,
"high_estimate": DECIMAL_FRACTION,
"headline_return": DECIMAL_FRACTION,
"model_implied_return": DECIMAL_FRACTION,
"pm_return": DECIMAL_FRACTION,
"scenario_ev": DECIMAL_FRACTION,
"bear_return": DECIMAL_FRACTION,
"neutral_return": DECIMAL_FRACTION,
"bull_return": DECIMAL_FRACTION,
"value_implied": DECIMAL_FRACTION,
"upside": DECIMAL_FRACTION,
"downside": DECIMAL_FRACTION,
"discount_to_price": DECIMAL_FRACTION,
"premium_to_price": DECIMAL_FRACTION,
"return_3m": DECIMAL_FRACTION,
"return_12m": DECIMAL_FRACTION,
# growth
"eps_growth": DECIMAL_FRACTION,
"analyst_upside": DECIMAL_FRACTION,
"margin_trend": DECIMAL_FRACTION,
"rev_growth": DECIMAL_FRACTION,
"revenue_growth": DECIMAL_FRACTION,
"fcf_growth": DECIMAL_FRACTION,
"terminal_growth": DECIMAL_FRACTION,
"consensus_growth": DECIMAL_FRACTION,
# margins
"op_margin_current": DECIMAL_FRACTION,
"op_margin_predicted": DECIMAL_FRACTION,
"operating_margin": DECIMAL_FRACTION,
"operating_margin_ttm": DECIMAL_FRACTION,
"operating_margin_forward_12m": DECIMAL_FRACTION,
"operating_margin_story_maturity": DECIMAL_FRACTION,
"net_margin": DECIMAL_FRACTION,
"net_margin_forward_implied": DECIMAL_FRACTION,
"gross_margin": DECIMAL_FRACTION,
"fcf_margin": DECIMAL_FRACTION,
"margin_change": DECIMAL_FRACTION,
# yields, rates
"dividend_yield": DECIMAL_FRACTION,
"fcf_yield": DECIMAL_FRACTION,
"sales_yield": DECIMAL_FRACTION,
"earnings_yield": DECIMAL_FRACTION,
"risk_free_rate": DECIMAL_FRACTION,
"equity_risk_premium": DECIMAL_FRACTION,
"discount_rate": DECIMAL_FRACTION,
"cost_of_equity": DECIMAL_FRACTION,
"wacc": DECIMAL_FRACTION,
"tax_rate": DECIMAL_FRACTION,
"effective_tax_rate": DECIMAL_FRACTION,
"roe": DECIMAL_FRACTION,
"roic": DECIMAL_FRACTION,
"nim": DECIMAL_FRACTION,
"cet1": DECIMAL_FRACTION,
"market_share": DECIMAL_FRACTION,
# probabilities and confidence
"p_bear": DECIMAL_FRACTION,
"p_neutral": DECIMAL_FRACTION,
"p_bull": DECIMAL_FRACTION,
"probability": DECIMAL_FRACTION,
"confidence": DECIMAL_FRACTION,
"beat_rate": DECIMAL_FRACTION,
"direction_accuracy": DECIMAL_FRACTION,
"rank_correlation": RATIO, # a correlation is not a percentage
# risk
"volatility": DECIMAL_FRACTION,
"drawdown": DECIMAL_FRACTION,
"max_drawdown": DECIMAL_FRACTION,
"tracking_error": DECIMAL_FRACTION,
# surprise
"avg_surprise": PERCENT_NUMBER, # providers report surprise as a percent number
"surprise_pct": PERCENT_NUMBER,
# spreads
"spread_bps": BASIS_POINTS,
"credit_spread_bps": BASIS_POINTS,
# multiples — ratios, never rendered with a percent sign
"pe_forward": RATIO,
"pe_trailing": RATIO,
"peg_forward": RATIO,
"ev_ebitda": RATIO,
"price_to_book": RATIO,
"debt_to_equity": RATIO,
}
def unit_of(field, default=None):
"""The declared storage unit of a field. Raises rather than guessing when undeclared."""
u = FIELD_UNITS.get(field, default)
if u is None:
raise UnitError(f"field '{field}' has no declared storage unit — add it to FIELD_UNITS")
return u
def to_decimal(value, unit=DECIMAL_FRACTION):
"""Convert any declared rate unit to a decimal fraction."""
if value is None:
return None
if unit == RATIO:
raise UnitError("a RATIO is not a percentage and has no decimal-fraction form")
try:
f = _TO_DECIMAL[unit]
except KeyError:
raise UnitError(f"unknown storage unit {unit!r}") from None
return float(value) * f
# ── the one percentage type ───────────────────────────────────────────────────────────
@dataclass(frozen=True)
class Pct:
"""A rate, held as a decimal fraction whatever unit it arrived in."""
decimal: float
@classmethod
def of(cls, value, unit=DECIMAL_FRACTION):
d = to_decimal(value, unit)
return None if d is None else cls(float(d))
@classmethod
def of_field(cls, field, value):
return cls.of(value, unit_of(field))
def as_decimal(self):
return self.decimal
def as_percent_number(self):
return self.decimal * 100.0
def as_basis_points(self):
return self.decimal * 10000.0
def format_percent(self, decimals=1, sign=False):
return format_percent(self.decimal, decimals=decimals, sign=sign)
def format_basis_points(self, decimals=0):
return f"{self.decimal * 10000.0:,.{decimals}f} bps"
def format_ratio(self, decimals=4):
return f"{self.decimal:.{decimals}f}"
def __str__(self):
return self.format_percent()
def format_percent(value, decimals=1, sign=False, unit=DECIMAL_FRACTION, none="n/a"):
"""The only way a percentage becomes user-visible text.
``format_percent(0.2375, sign=True) == "+23.8%"``. Passing a percent number by mistake is not
silently accepted: declare the unit instead — ``format_percent(23.75, unit=PERCENT_NUMBER)``.
"""
if isinstance(value, Pct):
value, unit = value.decimal, DECIMAL_FRACTION
if value is None:
return none
d = to_decimal(value, unit) * 100.0
if sign:
# a value that rounds to zero prints without a misleading direction
if round(d, decimals) == 0:
return f"{0.0:.{decimals}f}%"
return f"{d:+.{decimals}f}%"
return f"{d:.{decimals}f}%"
def format_pp(value, decimals=1, sign=True, unit=DECIMAL_FRACTION, none="n/a"):
"""A difference between two percentages, rendered in percentage points."""
if isinstance(value, Pct):
value, unit = value.decimal, DECIMAL_FRACTION
if value is None:
return none
d = to_decimal(value, unit) * 100.0
return f"{d:+.{decimals}f} pp" if sign else f"{d:.{decimals}f} pp"
def format_ratio(value, decimals=2, suffix="x", none="n/a"):
"""A multiple. Never carries a percent sign."""
if value is None:
return none
return f"{float(value):.{decimals}f}{suffix}"
_PCT_RE = re.compile(r"([+-]?\d{1,3}(?:,\d{3})*(?:\.\d+)?)\s*%")
_BPS_RE = re.compile(r"([+-]?\d{1,3}(?:,\d{3})*(?:\.\d+)?)\s*bps", re.I)
# ── deterministic ranges ──────────────────────────────────────────────────────────────
@dataclass(frozen=True)
class RangeClaim:
"""A numeric interval derived from registered endpoint claims, never invented in prose."""
lower: float
upper: float
semantic_type: str = DECIMAL_FRACTION
claim_id: str | None = None
lower_claim: str | None = None
upper_claim: str | None = None
label: str = ""
@classmethod
def from_endpoints(cls, a, b, semantic_type=DECIMAL_FRACTION, **kw):
"""Order-independent: the caller supplies two endpoint values, not a pre-sorted pair."""
if a is None or b is None:
return None
lo, hi = (a, b) if a <= b else (b, a)
return cls(float(lo), float(hi), semantic_type, **kw)
def magnitude(self):
"""The interval of absolute sizes — what "11–14% below price" states."""
a, b = abs(self.lower), abs(self.upper)
return (min(a, b), max(a, b))
def format_percent_range(self, decimals=0, absolute=False):
lo, hi = self.magnitude() if absolute else (self.lower, self.upper)
u = self.semantic_type
return f"{format_percent(lo, decimals, unit=u)}–{format_percent(hi, decimals, unit=u)}".replace("%–", "–")
def format_currency_range(self, decimals=2, symbol="$"):
lo, hi = (self.lower, self.upper)
return f"{symbol}{lo:,.{decimals}f}–{symbol}{hi:,.{decimals}f}"
def contains(self, value, tol=0.005):
return value is not None and (self.lower - tol) <= value <= (self.upper + tol)
_RANGE_PCT_RE = re.compile(r"(\d{1,3}(?:\.\d+)?)\s*[–-]\s*(\d{1,3}(?:\.\d+)?)\s*%")
_RANGE_CUR_RE = re.compile(r"\$(\d[\d,]*(?:\.\d+)?)\s*[–-]\s*\$?(\d[\d,]*(?:\.\d+)?)")
# ── guard against the defect returning ────────────────────────────────────────────────
_ADHOC = re.compile(r"""\{[^{}]*?:[+ ]?[,]?\.\d+f\}\s*%""")
def scan_source_for_adhoc_percent(source, allow_marker="# pct-ok"):
"""Report f-string sites that append a literal '%' to a formatted float.
Such a site re-implements ``format_percent`` and is where the unit convention silently
diverges. A line may opt out with a trailing ``# pct-ok`` marker when the value is genuinely
already a percent number from an external source.
"""
hits = []
for i, line in enumerate(source.splitlines(), 1):
if allow_marker in line:
continue
if _ADHOC.search(line):
hits.append((i, line.strip()))
return hits