File size: 13,135 Bytes
742f5b8 c9fa753 742f5b8 c9fa753 3519853 33b81e9 3519853 33b81e9 bc56496 0387621 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 | """Targeted tests for the semantic-type, label-propagation, final-object, reliability-language and
causal-polarity work. Section numbers refer to the remediation brief (§27 A–L, §28 properties)."""
import os, sys, importlib.util, pathlib
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import pytest
import concepts as cc
import policy as pol
import valuation_engines as ve
import canonical as cn
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import report_model as rm # noqa: E402 (the one audited renderer)
import pipeline_fixtures as pf # noqa: E402
def _sa_ok():
return {"ok": True, "blocking": [], "checks": [{"id": "A1", "ok": True}], "warnings": []}
def _kw(**over):
d = dict(policy={"issuer_class": "industrial", "disallowed_scorecard_rows": []}, scorecard={"metrics": []},
concepts={"revenue": cc.NET_REVENUE}, render_audit={"ok": True, "detail": ""},
metrics={"price": {"normalized_value": 1.0}}, text_audit_unresolved=[],
engine={"status": "OK", "name": "e", "display_name": "e"})
d.update(over); return d
# ── §27 A — correct engine, wrong renderer label ───────────────────────────────────────
# ── §27 B/C — same concept, different currency or scale ────────────────────────────────
def test_B_same_concept_mismatched_currency_is_blocked_until_normalized():
twd = cc.metric(cc.NET_REVENUE, 4800.0, metric_id="fc", raw_currency="TWD", raw_scale=cc.BILLION, unit="USD", period_type="NTM")
usd = cc.metric(cc.NET_REVENUE, 170.0, metric_id="cons", raw_currency="USD", raw_scale=cc.BILLION, unit="USD", period_type="NTM")
assert twd["normalized_value"] is None and "no FX rate" in twd["normalization_steps"][-1]
ok, why = cc.compatible(twd, usd, "level")
assert not ok and "unnormalized" in why
with pytest.raises(cc.IncompatibleMetricsError):
cc.derive(cc.GROWTH_RATE, lambda a, b: a / b - 1, [twd, usd], kind="level")
twd_ok = cc.metric(cc.NET_REVENUE, 4800.0, metric_id="fc", raw_currency="TWD", raw_scale=cc.BILLION, fx_to_usd=0.0316, unit="USD", period_type="NTM")
assert abs(twd_ok["normalized_value"] - 151.68e9) < 1e6 and cc.compatible(twd_ok, usd, "level")[0]
def test_C_same_currency_mismatched_scale_normalizes_or_blocks():
mm = cc.metric(cc.NET_REVENUE, 170_000.0, metric_id="a", raw_scale=cc.MILLION, unit="USD", period_type="NTM")
bn = cc.metric(cc.NET_REVENUE, 170.0, metric_id="b", raw_scale=cc.BILLION, unit="USD", period_type="NTM")
assert mm["normalized_value"] == bn["normalized_value"] == 170e9
assert mm["normalized_scale"] == bn["normalized_scale"] == cc.UNIT and cc.compatible(mm, bn, "level")[0]
# ── §27 D — ADS price with ordinary-share EPS ──────────────────────────────────────────
def test_D_ads_price_with_ordinary_share_eps_is_blocked():
price = cc.metric(cc.PRICE, 434.0, metric_id="price", unit="per share", period_type="spot",
security_basis=cc.ADS, per_share_basis=cc.PER_SHARE)
eps_ord = cc.metric(cc.GAAP_EPS, 1.41, metric_id="eps_ord", unit="per share", period_type="TTM",
security_basis=cc.ORDINARY_SHARE, per_share_basis=cc.PER_SHARE)
ok, why = cc.compatible(price, eps_ord, "multiple")
assert not ok and "security basis" in why
with pytest.raises(cc.IncompatibleMetricsError):
cc.derive(cc.PE_RATIO, lambda p, e: p / e, [price, eps_ord], kind="multiple")
eps_ads = cc.metric(cc.GAAP_EPS, 1.41, metric_id="eps_ads", unit="per share", period_type="TTM",
security_basis=cc.ORDINARY_SHARE, target_basis=cc.ADS, adr_ads_ratio=5, per_share_basis=cc.PER_SHARE)
pe = cc.derive(cc.PE_RATIO, lambda p, e: p / e, [price, eps_ads], kind="multiple", unit="x")
assert abs(eps_ads["normalized_value"] - 7.05) < 0.01 and abs(pe["value"] - 61.6) < 0.5
# ── §27 E — peer multiple inconsistent with price ÷ EPS ────────────────────────────────
def test_E_peer_pe_inconsistent_with_price_over_eps_is_removed():
# the panel's P/E was computed from ORDINARY-share earnings against an ADS price: 434 ÷ 1.41 ≈ 308x,
# while the report's own price ÷ per-ADS EPS is 434 ÷ 7.04 ≈ 62x
d = {"peer_comps": {"subject": {"ticker": "T", "pe": 307.8}, "peers": [{"ticker": "P", "pe": 30.0, "ps": 9.0, "op_margin": 0.05}]}}
rec = cn.reconcile_peer_table(d, {"price": {"value": 434.0}, "eps_ttm_profile": {"value": 7.04}})
assert rec["subject_ok"] is False and d["peer_comps"]["subject"]["pe"] is None
assert d["peer_comps"]["peers"][0]["pe"] is None and "peer_table_subject_pe" in d["rejected_fields"]
# ── §27 F — displayed scenarios always get arithmetic validation ───────────────────────
# ── §27 G — LIMITED reliability with "heavily backtested" prose ────────────────────────
# ── §27 H — an UNRELIABLE model's numbers never reach a persona ────────────────────────
# ── §27 I — intrinsic negative, pricing positive, momentum positive ────────────────────
# ── §27 J — causal polarity of a thesis trigger ────────────────────────────────────────
# ── §27 K — an executive from another organisation ─────────────────────────────────────
# ── §27 L — unresolved MATERIAL text-audit claim blocks publication ────────────────────
# ── §28 property tests ─────────────────────────────────────────────────────────────────
def test_P1_raw_currency_change_with_same_normalized_value_does_not_move_valuation():
a = cc.metric(cc.NET_REVENUE, 100.0, raw_currency="USD", raw_scale=cc.BILLION, unit="USD")
b = cc.metric(cc.NET_REVENUE, 100_000.0 / 0.0316 * 0.0316, raw_currency="USD", raw_scale=cc.MILLION, unit="USD")
c_ = cc.metric(cc.NET_REVENUE, 100.0 / 0.0316, raw_currency="TWD", raw_scale=cc.BILLION, fx_to_usd=0.0316, unit="USD")
assert abs(a["normalized_value"] - c_["normalized_value"]) < 1e3 and abs(a["normalized_value"] - b["normalized_value"]) < 1e3
def test_P2_ads_ratio_change_leaves_market_cap_and_valuation_unchanged():
for k in (1, 2, 5, 10):
px = 434.0 * (k / 5.0) # price per ADS scales with the ratio
eps = cc.metric(cc.GAAP_EPS, 1.41, unit="per share", security_basis=cc.ORDINARY_SHARE,
target_basis=cc.ADS, adr_ads_ratio=k, per_share_basis=cc.PER_SHARE, period_type="TTM")
shares_ads = 25.9e9 / k
assert abs(px * shares_ads - 434.0 * (25.9e9 / 5.0)) < 1e6 # market cap invariant
assert abs((px / eps["normalized_value"]) - (434.0 / 7.05)) < 0.5 # P/E invariant
def test_P3_reliability_downgrade_never_increases_weight_or_strength():
v = pol.quant_reliability({"dir_acc": 0.62, "rank_ic": 0.3, "n_oos_eff": 12})
u = pol.quant_reliability({"dir_acc": 0.40, "rank_ic": 0.0, "n_oos_eff": 5})
assert u["max_blend_weight"] <= v["max_blend_weight"] and not u["persona_visibility"] and v["persona_visibility"]
assert set(v["forbidden_phrases"]) <= set(u["forbidden_phrases"])
def test_P4_engine_change_changes_the_methodology_label_automatically():
a = ve.select_engine("FcffValuation").metadata(); b = ve.select_engine("BankEquityValuation").metadata()
assert a["display_name"] != b["display_name"] and a["methodology_description"] != b["methodology_description"]
assert "FCFF" in a["allowed_renderer_terms"] and "FCFF" in b["forbidden_renderer_terms"]
def test_P5_displayed_odds_force_validation():
s = cc.scenario_set_from(0.3, 0.3, 0.4, -0.08, 0.19, 0.30, 0.20)
assert s.validate()[0] == "FAIL" # 0.133 ≠ 0.20
ok = cc.scenario_set_from(0.3, 0.3, 0.4, -0.08, 0.19, 0.30, 0.3 * -0.08 + 0.3 * 0.19 + 0.4 * 0.30)
assert ok.validate()[0] == "PASS"
def test_P7_trigger_polarity_change_changes_the_flip_language():
up = cc.build_triggers([{"metric": "operating margin", "direction": "up", "operator": ">=", "threshold": "6%", "scenario": "BULL"}])[0]
dn = cc.build_triggers([{"metric": "operating margin", "direction": "down", "operator": "<=", "threshold": "4%", "scenario": "BEAR"}])[0]
assert "bull case" in up.sentence() and "bear case" in dn.sentence()
assert up.polarity == cc.POSITIVE_FOR_EQUITY and dn.polarity == cc.NEGATIVE_FOR_EQUITY
# ── §29 invariants not covered above ───────────────────────────────────────────────────
def test_inv_models_never_consume_raw_when_normalized_exists():
m = cc.metric(cc.NET_REVENUE, 4800.0, raw_currency="TWD", raw_scale=cc.BILLION, fx_to_usd=0.0316, unit="USD")
assert m["value"] == m["normalized_value"] != m["raw_value"]
def test_inv_final_object_version_must_appear_in_the_document():
run, _, _ = pf.run_pipeline()
import catalog as C
rep = rm.build_user_report(run.state, C.build_catalog(run.state), {"summary": [], "thesis": [], "risks": [], "seats": {}}, [])
assert rm.audit(rep, run.state, rm.render_markdown(rep), rm.render_html(rep)).passed
for blk in rep.blocks:
blk.content = [f for f in blk.content if getattr(f, "claim_id", None) != "VERSION"] if blk.kind == "para" else blk.content
out = rm.audit(rep, run.state, rm.render_markdown(rep), rm.render_html(rep))
assert "R2" in {c.id for c in out.checks if c.status == "FAIL"}
# ── false-positive regressions found in the acceptance runs ────────────────────────────
def test_reg_foreign_private_issuer_on_a_us_exchange_is_ads_quoted():
sub = {"cik": 1, "sub": {"filings": {"recent": {"form": ["20-F", "6-K"]}}}, }
sub["sub"]["exchanges"] = ["NYSE", "OTC"]
iss = cn.classify_issuer("TSM", {"Name": "Taiwan Semiconductor Manufacturing Company Limited", "Country": "USA",
"Industry": "SEMICONDUCTORS"}, sub, {"currency": "TWD", "accounting_standard": "IFRS"})
assert iss["is_fpi"] and iss["is_adr"], "an FPI listed on a US exchange trades as ADS"
# ---------------------------------------------------------------------------
# §30 Consensus boundary normalization
# ---------------------------------------------------------------------------
def _consdat(**over):
d = {"fx_usd": 0.031674, "overview": {"RevenueTTM": 1.41e11, "EPS": 13.39},
"income": {"quarterlyReports": []},
"earnings_estimates_consensus": {"current_year_revenue": 5.4422750681e12,
"next_year_revenue": 6.85e12,
"current_year_eps": 16.93, "next_year_eps": 21.93,
"analyst_count": 13}}
d["earnings_estimates_consensus"].update(over)
return d
def test_filing_currency_consensus_is_converted_before_any_comparison():
"""A TWD consensus in a USD-labelled field must not be compared with a USD forecast."""
d = _consdat()
notes = cn.normalize_consensus(d)
ee = d["earnings_estimates_consensus"]
assert notes and "consensus revenue" in notes[0]
assert 1.5e11 < ee["current_year_revenue"] < 2.0e11, "consensus revenue must land near the USD anchor"
assert ee["next_year_revenue"] > ee["current_year_revenue"], "the group converts together"
assert ee["current_year_eps"] == 16.93, "an already-USD field is left untouched"
def test_consensus_normalization_is_idempotent():
d = _consdat()
first = cn.normalize_consensus(d)
rev = d["earnings_estimates_consensus"]["current_year_revenue"]
assert cn.normalize_consensus(d) == first
assert d["earnings_estimates_consensus"]["current_year_revenue"] == rev, "no double conversion"
def test_unresolvable_consensus_is_withheld_not_published():
d = _consdat(current_year_revenue=9.9e17, next_year_revenue=1.0e18)
notes = cn.normalize_consensus(d)
assert any("UNIT_UNRESOLVED" in n for n in notes)
assert "current_year_revenue" not in d["earnings_estimates_consensus"]
def test_single_currency_issuer_needs_no_consensus_conversion():
d = _consdat()
d["fx_usd"] = 1.0
d["earnings_estimates_consensus"]["current_year_revenue"] = 3.9e11
assert cn.normalize_consensus(d) == []
assert d["earnings_estimates_consensus"]["current_year_revenue"] == 3.9e11
|