Download engine/catalog.py from s-ttp/crucible: direct link, hf CLI and curl.
- Browser
- Download file 54.2 kB
-
https://huggingface.co/spaces/s-ttp/crucible/resolve/main/engine/catalog.py
- Command line
-
hf download hf://spaces/s-ttp/crucible/engine/catalog.py
-
curl -L -o catalog.py https://huggingface.co/spaces/s-ttp/crucible/resolve/main/engine/catalog.py
54.2 kB
| """The claim catalog: every material number a reader can see, with its identity attached. | |
| A claim is a typed pointer into the frozen AnalysisState. It has one id, one state path, one | |
| semantic type, one label, and a list of labels it must never be given. The renderer and the | |
| prose ingestion use claims exclusively, so the identity of a number is fixed when the number is | |
| produced and is never re-inferred from the words around it. | |
| Claim ids never collide: the headline return and the PM return are different claims with | |
| different labels, and each forbids the other's vocabulary next to it. | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass, field, replace | |
| from units import format_percent | |
| # semantic types → formatter | |
| CURRENCY, CURRENCY_BIG, PCT, PCT_SIGNED, GAP, MULTIPLE, RATIO, COUNT, TEXT, PP, COUNT_SIGNED, BPS = ( | |
| "CURRENCY", "CURRENCY_BIG", "PCT", "PCT_SIGNED", "GAP", "MULTIPLE", "RATIO", "COUNT", "TEXT", "PP", "COUNT_SIGNED", "BPS") | |
| # REL: a RelationshipClaim rendered as one complete clause; PHRASE: a signed quantity rendered with its own direction; | |
| # GUIDANCE: a typed guidance range (rendering is presentation, the row is the claim) | |
| REL, PHRASE, GUIDANCE = "REL", "PHRASE", "GUIDANCE" | |
| # a share printed to two decimals: the story's peak share, so market size × share reproduces the revenue at maturity at | |
| # the precision the report prints ($130.00B × 17.2% = $22.36B read against a stated $22.30B) | |
| PCT2 = "PCT2" | |
| SIGNED_STYPES = (PCT_SIGNED, COUNT_SIGNED) | |
| def fmt(stype, v): | |
| if v is None: | |
| return None | |
| if stype == CURRENCY: | |
| return f"${v:,.2f}" | |
| if stype == CURRENCY_BIG: | |
| a = abs(v) | |
| sign = "-" if v < 0 else "" | |
| if a >= 1e12: | |
| return f"{sign}${a / 1e12:,.2f}T" | |
| if a >= 1e9: | |
| return f"{sign}${a / 1e9:,.2f}B" | |
| if a >= 1e6: | |
| return f"{sign}${a / 1e6:,.1f}M" | |
| return f"{sign}${a:,.0f}" | |
| if stype == PCT: | |
| return format_percent(v, 1) | |
| if stype == PCT2: | |
| return format_percent(v, 2) | |
| if stype == PCT_SIGNED: | |
| return format_percent(v, 1, sign=True) | |
| if stype == GAP: | |
| if abs(v) < 0.0005: | |
| return "in line with" | |
| return f"{format_percent(abs(v), 1)} {'above' if v > 0 else 'below'}" | |
| if stype == MULTIPLE: | |
| return f"{v:,.1f}x" | |
| if stype == RATIO: | |
| return f"{v:,.2f}" | |
| if stype == COUNT: | |
| return f"{int(round(v)):,}" | |
| if stype == COUNT_SIGNED: | |
| return f"{int(round(v)):+,}" | |
| if stype == BPS: # a canonical decimal change (0.0035) stated in basis points | |
| return f"{v * 10000.0:,.0f} bps" | |
| if stype == PP: | |
| return f"{v * 100:+.1f} pts" | |
| return str(v) | |
| class ClaimDef: | |
| claim_id: str | |
| path: str | |
| stype: str | |
| label: str | |
| prohibited: tuple = () | |
| deferred: bool = False # only exists after the panel / blend (post-debate) | |
| # the words that must label this claim in prose (at least one, in the words before it): a stable-period | |
| # rate cited without "stable"/"mature"/"terminal" reads as today's rate | |
| required_qualifiers: tuple = () | |
| persona_visible: bool = True # False: rendered by the report, not offered to personas (redundant there) | |
| label_path: str | None = None # the label a reader sees comes from the state (e.g. a pro-forma story base) | |
| phrase: tuple = () # PHRASE: (template if > 0, template if < 0, template if 0, magnitude stype) | |
| render_mode: str = "FULL" # REL: FULL (a complete clause), CLAUSE or COMPACT (a predicate after its lhs subject) | |
| clause: bool = False # renders a complete clause: it cannot serve as a noun phrase inside another clause | |
| slot: str = "" # PREDICATE: renders a predicate ("down 11.8% …"), after a verb or its subject | |
| unit_after: str = "" # a bare count whose unit the reader needs right after it ("10" → "10 years") | |
| names: tuple = () # how a reader names this quantity without citing it ("the growth the price demands") | |
| _HEADLINE_WORDS = ("headline", "blended", "model-implied", "overall 12-month", "final return") | |
| _PM_WORDS = ("panel", "PM", "portfolio manager", "scenario-weighted", "probability-weighted") | |
| _FORECAST_WORDS = ("guidance", "consensus", "company expects", "management expects") | |
| _CONSENSUS_WORDS = ("our forecast", "engine forecast", "the model forecasts", "guidance") | |
| _STORY_WORDS = ("next year", "next-year", "12-month", "forecast", "guidance", "consensus") | |
| _INTRINSIC_WORDS = ("target", "consensus", "market composite", "analyst") | |
| _COMPOSITE_WORDS = ("intrinsic", "fair value", "worth", "DCF") | |
| CLAIMS = [ | |
| ClaimDef("PRICE", "market.price", CURRENCY, "share price"), | |
| ClaimDef("MARKET_CAP", "market.market_cap", CURRENCY_BIG, "market capitalisation"), | |
| ClaimDef("DIVIDEND_YIELD", "market.dividend_yield", PCT, "trailing dividend yield"), | |
| ClaimDef("RETURN_3M", "market.return_3m", PCT_SIGNED, "3-month price return"), | |
| ClaimDef("RETURN_12M", "market.return_12m", PCT_SIGNED, "12-month price return"), | |
| ClaimDef("SMA200", "market.sma200", CURRENCY, "200-day moving average"), | |
| ClaimDef("REVENUE_TTM", "historical.revenue_ttm", CURRENCY_BIG, "trailing twelve-month revenue", _FORECAST_WORDS + ("forecast",)), | |
| ClaimDef("OPERATING_INCOME_TTM", "historical.operating_income_ttm", CURRENCY_BIG, "trailing operating income"), | |
| ClaimDef("OPERATING_MARGIN_TTM", "historical.operating_margin_ttm", PCT, "trailing operating margin", | |
| ("forecast", "next year", "guidance", "story", "maturity")), | |
| ClaimDef("NET_INCOME_TTM", "historical.net_income_ttm", CURRENCY_BIG, "trailing net income"), | |
| ClaimDef("NET_MARGIN_TTM", "historical.net_margin_ttm", PCT, "trailing net margin", ("forecast", "next year")), | |
| ClaimDef("EPS_ADJUSTED_TTM", "historical.eps_adjusted_ttm", CURRENCY, "trailing adjusted EPS", ("GAAP", "reported")), | |
| ClaimDef("EPS_GAAP_TTM", "historical.eps_gaap_ttm", CURRENCY, "trailing reported EPS", ("adjusted",)), | |
| ClaimDef("BEAT_RATE", "historical.beat_rate_8q", PCT, "share of recent quarters beating estimates"), | |
| ClaimDef("FORECAST_REVENUE", "forecast.revenue", CURRENCY_BIG, "engine forecast revenue (next 12 months)", _FORECAST_WORDS), | |
| ClaimDef("FORECAST_REVENUE_GROWTH", "forecast.revenue_growth", PCT_SIGNED, "engine forecast revenue growth", _FORECAST_WORDS), | |
| ClaimDef("FORECAST_OPERATING_MARGIN", "forecast.operating_margin", PCT, "engine forecast operating margin", | |
| _FORECAST_WORDS + ("trailing", "story", "maturity")), | |
| ClaimDef("FORECAST_NET_MARGIN", "forecast.net_margin", PCT, "engine forecast net margin", _FORECAST_WORDS), | |
| ClaimDef("FORECAST_OPERATING_MARGIN_CHANGE", "forecast.operating_margin_change", PP, | |
| "change in operating margin from the trailing twelve months to the engine forecast", _FORECAST_WORDS), | |
| ClaimDef("FORECAST_EPS", "forecast.eps", CURRENCY, "engine forecast EPS (next 12 months)", _FORECAST_WORDS), | |
| ClaimDef("FORECAST_EPS_GROWTH", "forecast.eps_growth", PCT_SIGNED, "engine forecast EPS growth", _FORECAST_WORDS), | |
| ClaimDef("FORECAST_EPS_VS_CONSENSUS", "forecast.eps_vs_consensus", GAP, "engine EPS relative to consensus for the same twelve months"), | |
| ClaimDef("CONSENSUS_EPS_NTM", "consensus.eps_ntm", CURRENCY, "sell-side consensus EPS for the forecast's twelve months (time-weighted FY1/FY2)", _CONSENSUS_WORDS), | |
| ClaimDef("CONSENSUS_EPS_FY1", "consensus.eps_fy1", CURRENCY, "sell-side consensus EPS (current fiscal year)", _CONSENSUS_WORDS), | |
| ClaimDef("CONSENSUS_EPS_FY2", "consensus.eps_fy2", CURRENCY, "sell-side consensus EPS (next fiscal year)", _CONSENSUS_WORDS), | |
| ClaimDef("CONSENSUS_REVENUE_FY1", "consensus.revenue_fy1", CURRENCY_BIG, "sell-side consensus revenue (current fiscal year)", _CONSENSUS_WORDS), | |
| ClaimDef("CONSENSUS_REVENUE_NTM", "consensus.revenue_ntm", CURRENCY_BIG, | |
| "sell-side consensus revenue for the forecast's twelve months (time-weighted FY1/FY2)", _CONSENSUS_WORDS), | |
| ClaimDef("CONSENSUS_ANALYSTS", "consensus.n_analysts", COUNT, "estimate contributors"), | |
| ClaimDef("ANALYST_TARGET", "consensus.target_price", CURRENCY, "raw mean sell-side price target (as published)", | |
| ("intrinsic", "fair value", "our valuation")), | |
| ClaimDef("TARGET_VS_PRICE", "pricing.target_vs_price", PCT_SIGNED, "upside to the mean analyst target"), | |
| ClaimDef("INTRINSIC_VALUE", "valuation.intrinsic_value", CURRENCY, "intrinsic value", _INTRINSIC_WORDS), | |
| ClaimDef("PRICE_VS_INTRINSIC", "valuation.price_vs_intrinsic", GAP, "price relative to intrinsic value"), | |
| ClaimDef("MECHANICAL_VALUE", "valuation.mechanical_value", CURRENCY, "cross-check value", _INTRINSIC_WORDS), | |
| ClaimDef("PRICE_VS_MECHANICAL", "valuation.price_vs_mechanical", GAP, "price relative to the cross-check value"), | |
| ClaimDef("MARKET_COMPOSITE", "pricing.market_composite", CURRENCY, "market composite (pricing)", _COMPOSITE_WORDS), | |
| ClaimDef("PRICE_VS_COMPOSITE", "pricing.price_vs_composite", GAP, "price relative to the market composite"), | |
| ClaimDef("MARKET_COMPOSITE_RAW", "pricing.raw_composite", CURRENCY, "market composite before trimming and caps", _COMPOSITE_WORDS), | |
| ClaimDef("MARKET_COMPOSITE_POST_TRIM", "pricing.post_trim_composite", CURRENCY, "market composite after outlier trimming", | |
| _COMPOSITE_WORDS), | |
| ClaimDef("ANALYST_TARGET_ADJUSTED", "pricing.analyst_target_adjusted", CURRENCY, | |
| "risk-adjusted analyst-target contribution to the composite (not the published target)", ("intrinsic", "fair value")), | |
| ClaimDef("PE_FORWARD", "pricing.pe_forward", MULTIPLE, "forward P/E (engine EPS)"), | |
| ClaimDef("PE_TRAILING", "pricing.pe_trailing", MULTIPLE, "trailing P/E (adjusted EPS)"), | |
| ClaimDef("PE_TRAILING_GAAP", "pricing.pe_trailing_gaap", MULTIPLE, "trailing P/E (reported EPS)"), | |
| ClaimDef("PEG", "pricing.peg_forward", RATIO, "forward PEG"), | |
| ClaimDef("STORY_TAM", "valuation.story_tam", CURRENCY_BIG, "story market size at maturity"), | |
| ClaimDef("STORY_SHARE", "valuation.story_share", PCT2, "story peak market share"), | |
| ClaimDef("STORY_REVENUE_MATURITY", "valuation.story_revenue_maturity", CURRENCY_BIG, "story revenue at maturity"), | |
| ClaimDef("STORY_YEARS", "valuation.story_years", COUNT, "years to maturity"), | |
| ClaimDef("STORY_MARGIN", "valuation.story_maturity_margin", PCT, "story maturity operating margin assumption", _STORY_WORDS), | |
| ClaimDef("STORY_START_MARGIN", "valuation.story_base.operating_margin", PCT, "story starting operating margin", | |
| label_path="valuation.story_base.label"), | |
| ClaimDef("STORY_START_REVENUE", "valuation.story_base.revenue", CURRENCY_BIG, "story starting revenue", | |
| label_path="valuation.story_base.revenue_label"), | |
| ClaimDef("STORY_MARGIN_CHANGE", "valuation.story.margin_change", PP, | |
| "story operating-margin change from the starting margin to maturity", _STORY_WORDS), | |
| ClaimDef("STORY_WACC", "valuation.discount_rate_path.initial_wacc_metric", PCT, "initial cost of capital (years at the start)"), | |
| ClaimDef("STORY_WACC_STABLE", "valuation.discount_rate_path.terminal_wacc_metric", PCT, | |
| "terminal cost of capital used in the valuation (stable period)", | |
| ("current", "today", "initial", "now", "present"), | |
| required_qualifiers=("stable", "mature", "terminal", "long-run", "long run", "fading to", "fades to", "falls to", "declining to")), | |
| ClaimDef("STORY_WACC_HOLD", "valuation.discount_rate_path.hold_years", COUNT, "years at the initial cost of capital"), | |
| ClaimDef("STORY_TERMINAL_GROWTH", "valuation.story.assumptions.terminal_growth", PCT, "terminal growth"), | |
| ClaimDef("STORY_FAILURE", "valuation.story.assumptions.failure_probability", PCT, "failure probability"), | |
| ClaimDef("CEO", "leadership.roles.CEO.name", TEXT, "chief executive officer"), | |
| ClaimDef("CFO", "leadership.roles.CFO.name", TEXT, "chief financial officer"), | |
| ClaimDef("INSIDER_CHARACTERIZATION", "insiders.canon.characterization", TEXT, "insider transactions (SEC Form 4)"), | |
| ClaimDef("MOMENTUM", "market.momentum.state", TEXT, "momentum state"), | |
| ClaimDef("LIFECYCLE_STAGE", "valuation.lifecycle.stage", TEXT, "life-cycle stage (Damodaran corporate life cycle)"), | |
| ClaimDef("LIFECYCLE_TRAILING_GROWTH", "valuation.lifecycle_trailing_growth", PCT_SIGNED, "trailing revenue growth (year over year)", | |
| ("forecast", "consensus")), | |
| ClaimDef("LIFECYCLE_FORWARD_GROWTH", "valuation.lifecycle_forward_growth", PCT_SIGNED, "forward revenue growth (consensus, next fiscal year)", | |
| ("trailing",)), | |
| ClaimDef("BE_REVENUE", "valuation.breakeven_revenue", CURRENCY_BIG, "revenue today's price needs at maturity (break-even)"), | |
| ClaimDef("BE_REVENUE_CAGR", "valuation.breakeven_revenue_cagr", PCT, | |
| "annual revenue growth from the story's starting revenue to the break-even revenue (bound to that pair)"), | |
| ClaimDef("BE_MARGIN", "valuation.breakeven_margin", PCT, "operating margin today's price needs at maturity (break-even)"), | |
| ClaimDef("ML_RANK_SHARE", "quant.panel_rank.figures.rank_share", PCT, "share of ranked companies below this one"), | |
| ClaimDef("ML_RANK_DECILE", "quant.panel_rank.figures.decile", COUNT, "machine-learning rank decile"), | |
| ClaimDef("ML_RANK_UNIVERSE", "quant.panel_rank.figures.n_universe", COUNT, "companies ranked"), | |
| ClaimDef("ML_RANK_IC", "quant.panel_rank.reliability.rank_ic", RATIO, "walk-forward rank correlation"), | |
| ClaimDef("ML_RANK_POSITIVE", "quant.panel_rank.reliability.ic_positive_share", PCT, "share of test months the rank was right-ordered"), | |
| ClaimDef("DMD_INDUSTRY", "valuation.damodaran_inputs.industry", TEXT, "industry (Damodaran classification)"), | |
| ClaimDef("DMD_UNLEVERED_BETA", "valuation.damodaran_inputs.unlevered_beta", RATIO, "industry unlevered beta (cash-corrected)"), | |
| ClaimDef("DMD_BETA", "valuation.damodaran_inputs.beta", RATIO, "bottom-up beta (re-levered at the company's D/E)"), | |
| ClaimDef("DMD_DE", "valuation.damodaran_inputs.debt_to_equity", PCT, "market debt-to-equity"), | |
| ClaimDef("DMD_ERP", "valuation.damodaran_inputs.erp", PCT, "equity risk premium used"), | |
| ClaimDef("DMD_IMPLIED_ERP", "valuation.damodaran_inputs.implied_erp", PCT, "implied equity risk premium (Damodaran, monthly)"), | |
| ClaimDef("DMD_CRP", "valuation.damodaran_inputs.crp", PCT, "country risk premium"), | |
| ClaimDef("DMD_ERP_MONTH", "valuation.damodaran_inputs.erp_month", TEXT, "month of the implied ERP"), | |
| ClaimDef("DMD_COUNTRY", "valuation.damodaran_inputs.country", TEXT, "country of operations"), | |
| ClaimDef("DMD_S2C", "valuation.damodaran_inputs.industry_sales_to_capital", RATIO, "industry sales-to-invested-capital"), | |
| ClaimDef("WACC_BENCHMARK", "valuation.discount_rate_path.benchmark_wacc_metric", PCT, | |
| "mature-market cost-of-capital benchmark (a reference, not necessarily the rate used)", | |
| ("used", "current", "today", "initial", "our rate"), persona_visible=False), | |
| ClaimDef("DMD_MARGIN", "valuation.industry_operating_margin", PCT, "industry pre-tax operating margin (industry aggregate)"), | |
| ClaimDef("OWN_13F_AS_OF", "ownership.f13.as_of", TEXT, "13F quarter end"), | |
| ClaimDef("OWN_13F_HOLDERS", "ownership.f13.n_holders", COUNT, "institutions reporting a position (13F)"), | |
| ClaimDef("OWN_13F_HOLDERS_PREV", "ownership.f13.n_holders_prev", COUNT, "institutions reporting a position the quarter before"), | |
| ClaimDef("OWN_13F_TRACKED", "ownership.f13.n_gurus", COUNT, "tracked long-term investors holding"), | |
| ClaimDef("OWN_13F_NEW", "ownership.f13.guru_new_buys", COUNT, "tracked investors opening a position"), | |
| ClaimDef("OWN_13F_ADDS", "ownership.f13.guru_adds", COUNT, "tracked investors adding"), | |
| ClaimDef("OWN_13F_TRIMS", "ownership.f13.guru_trims", COUNT, "tracked investors trimming"), | |
| ClaimDef("OWN_13F_EXITS", "ownership.f13.guru_exits", COUNT, "tracked investors exiting"), | |
| ClaimDef("OWN_13F_NAMES", "ownership.f13.guru_names_short", TEXT, "tracked investors holding (names)"), | |
| ClaimDef("OWN_13F_HOLDERS_CHANGE", "ownership.f13.n_holders_change", COUNT_SIGNED, | |
| "net change in the number of institutions reporting a position vs the quarter before (signed; positions, not intent)"), | |
| ClaimDef("INSIDER_DISCRETIONARY_NET", "insiders.canon.materiality.discretionary_net_value", CURRENCY_BIG, | |
| "discretionary open-market purchases less sales in the window (SEC Form 4)"), | |
| ClaimDef("INSIDER_MATERIALITY", "insiders.canon.materiality.share_of_market_cap", PCT, | |
| "discretionary net insider trading as a share of market cap"), | |
| ClaimDef("ADJ_GAAP_BRIDGE", "historical.adjustment_bridge.selected_ratio", RATIO, | |
| "adjusted EPS ÷ GAAP diluted EPS, like for like (the adjusted↔GAAP bridge)"), | |
| ClaimDef("SEGMENTS_COVERAGE", "segments.canonical.coverage", PCT, | |
| "filed segment revenue as a share of consolidated revenue, same period (the business AS FILED)"), | |
| ClaimDef("SEGMENT_COUNT_FILED", "valuation.segment_count_filed", COUNT, | |
| "reportable segments in the latest filing (the business AS FILED, before any separation)"), | |
| ClaimDef("SEGMENT_COUNT_CONTINUING", "valuation.segment_count_continuing", COUNT, | |
| "businesses in the continuing company (the separated segment removed)"), | |
| # post-debate claims | |
| ClaimDef("SCENARIO_BEAR_RETURN", "scenarios.bear.return", PCT_SIGNED, "bear scenario price return", deferred=True), | |
| ClaimDef("SCENARIO_NEUTRAL_RETURN", "scenarios.neutral.return", PCT_SIGNED, "neutral scenario price return", deferred=True), | |
| ClaimDef("SCENARIO_BULL_RETURN", "scenarios.bull.return", PCT_SIGNED, "bull scenario price return", deferred=True), | |
| ClaimDef("SCENARIO_BEAR_PROB", "scenarios.bear.probability", PCT, "bear scenario probability", deferred=True), | |
| ClaimDef("SCENARIO_NEUTRAL_PROB", "scenarios.neutral.probability", PCT, "neutral scenario probability", deferred=True), | |
| ClaimDef("SCENARIO_BULL_PROB", "scenarios.bull.probability", PCT, "bull scenario probability", deferred=True), | |
| ClaimDef("PM_RETURN", "scenarios.pm_return", PCT_SIGNED, "panel probability-weighted scenario return", _HEADLINE_WORDS, deferred=True), | |
| ClaimDef("HEADLINE_RETURN", "blend.headline_return", PCT_SIGNED, "blended model-implied 12-month return", _PM_WORDS, deferred=True), | |
| ClaimDef("RATING", "recommendation.rating", TEXT, "rating", deferred=True), | |
| ClaimDef("RANGE_LOW", "uncertainty.scenario_range.low", PCT_SIGNED, "bear-case end of the scenario range", deferred=True), | |
| ClaimDef("RANGE_HIGH", "uncertainty.scenario_range.high", PCT_SIGNED, "bull-case end of the scenario range", deferred=True), | |
| # ── RelationshipClaims: each renders lhs, relation and rhs as one clause (relations.py) ── | |
| ClaimDef("REL_PRICE_INTRINSIC", "relations.state.items.REL_PRICE_INTRINSIC", REL, "price vs intrinsic value (complete relation)"), | |
| ClaimDef("REL_PRICE_MECHANICAL", "relations.state.items.REL_PRICE_MECHANICAL", REL, "price vs cross-check value (complete relation)"), | |
| ClaimDef("REL_PRICE_MARKET_COMPOSITE", "relations.state.items.REL_PRICE_MARKET_COMPOSITE", REL, | |
| "price vs market composite (complete relation)"), | |
| ClaimDef("REL_TARGET_PRICE", "relations.state.items.REL_TARGET_PRICE", REL, "mean analyst target vs price (complete relation)"), | |
| ClaimDef("REL_FORECAST_EPS_CONSENSUS", "relations.state.items.REL_FORECAST_EPS_CONSENSUS", REL, | |
| "engine EPS forecast vs consensus for the same twelve months (complete relation)"), | |
| ClaimDef("REL_ENGINE_REVENUE_CONSENSUS", "relations.state.items.REL_ENGINE_REVENUE_CONSENSUS", REL, | |
| "engine revenue forecast vs consensus revenue for the same twelve months (complete relation)"), | |
| ClaimDef("REL_GUIDANCE_EPS_CONSENSUS", "relations.state.items.REL_GUIDANCE_EPS_CONSENSUS", REL, | |
| "company EPS guidance range vs consensus EPS for the same fiscal year and basis (complete relation)"), | |
| ClaimDef("REL_BREAK_EVEN_REVENUE_STORY", "relations.state.items.REL_BREAK_EVEN_REVENUE_STORY", REL, | |
| "break-even revenue vs the story's revenue at maturity (complete relation)"), | |
| ClaimDef("REL_BREAK_EVEN_MARGIN_STORY", "relations.state.items.REL_BREAK_EVEN_MARGIN_STORY", REL, | |
| "break-even operating margin vs the story's maturity margin (complete relation)"), | |
| ClaimDef("REL_BREAK_EVEN_REVENUE_GROWTH", "relations.state.items.REL_BREAK_EVEN_REVENUE_GROWTH", REL, | |
| "growth needed from the starting revenue to the break-even revenue (complete relation)"), | |
| ClaimDef("REL_OPERATING_MARGIN_INDUSTRY", "relations.state.items.REL_OPERATING_MARGIN_INDUSTRY", REL, | |
| "company operating margin vs the industry aggregate (complete relation)"), | |
| ClaimDef("REL_EXPECTATIONS_STORY", "relations.state.items.REL_EXPECTATIONS_STORY", REL, | |
| "what today's price requires vs the adopted story (the only permitted statement of that polarity)"), | |
| # ── signed quantities rendered with their own direction: the only form offered to authors ── | |
| ClaimDef("RETURN_3M_PHRASE", "market.return_3m", PHRASE, "3-month share-price move", | |
| phrase=("up {m} over three months", "down {m} over three months", "unchanged over three months", PCT)), | |
| ClaimDef("RETURN_12M_PHRASE", "market.return_12m", PHRASE, "12-month share-price move", | |
| phrase=("up {m} over twelve months", "down {m} over twelve months", "unchanged over twelve months", PCT)), | |
| ClaimDef("OWN_13F_HOLDERS_CHANGE_PHRASE", "ownership.f13.n_holders_change", PHRASE, | |
| "change in institutions reporting a position vs the quarter before, as a complete clause (positions, not intent)", | |
| phrase=("the number of institutions reporting a position rose by {m} from the quarter before", | |
| "the number of institutions reporting a position fell by {m} from the quarter before", | |
| "the number of institutions reporting a position was unchanged from the quarter before", COUNT), | |
| clause=True), | |
| ClaimDef("SCENARIO_BEAR_RETURN_PHRASE", "scenarios.bear.return", PHRASE, "bear scenario price return", deferred=True, | |
| phrase=("{a} {m} upside case", "{a} {m} downside case", "a flat case", PCT)), | |
| ClaimDef("SCENARIO_NEUTRAL_RETURN_PHRASE", "scenarios.neutral.return", PHRASE, "neutral scenario price return", deferred=True, | |
| phrase=("{a} {m} upside case", "{a} {m} downside case", "a flat case", PCT)), | |
| ClaimDef("SCENARIO_BULL_RETURN_PHRASE", "scenarios.bull.return", PHRASE, "bull scenario price return", deferred=True, | |
| phrase=("{a} {m} upside case", "{a} {m} downside case", "a flat case", PCT)), | |
| ClaimDef("PM_RETURN_PHRASE", "scenarios.pm_return", PHRASE, "panel probability-weighted scenario return", _HEADLINE_WORDS, | |
| deferred=True, phrase=("{a} {m} probability-weighted gain", "{a} {m} probability-weighted loss", | |
| "a flat probability-weighted return", PCT)), | |
| ClaimDef("HEADLINE_RETURN_PHRASE", "blend.headline_return", PHRASE, "blended model-implied 12-month return", _PM_WORDS, | |
| deferred=True, phrase=("{a} {m} model-implied gain", "{a} {m} model-implied loss", "a flat model-implied return", PCT)), | |
| ] | |
| # CLAUSE / COMPACT render modes of the two-sided relations: the same typed lhs, operator, magnitude and rhs, shorter | |
| _MODED = ("REL_PRICE_INTRINSIC", "REL_PRICE_MECHANICAL", "REL_PRICE_MARKET_COMPOSITE", "REL_TARGET_PRICE", | |
| "REL_FORECAST_EPS_CONSENSUS", "REL_ENGINE_REVENUE_CONSENSUS", "REL_BREAK_EVEN_REVENUE_STORY", | |
| "REL_BREAK_EVEN_MARGIN_STORY", "REL_OPERATING_MARGIN_INDUSTRY") | |
| for _cid in _MODED + ("REL_EXPECTATIONS_STORY",): | |
| for _mode in ("CLAUSE", "COMPACT", "PREDICATE", "NOUN_PHRASE", "FULL_SENTENCE"): | |
| if _cid == "REL_EXPECTATIONS_STORY" and _mode not in ("FULL_SENTENCE", "COMPACT"): | |
| continue | |
| CLAIMS.append(ClaimDef(f"{_cid}__{_mode}", f"relations.state.items.{_cid}", REL, | |
| f"{next(c.label for c in CLAIMS if c.claim_id == _cid).split(' (')[0]} ({_mode.lower()} form: " | |
| + {"FULL_SENTENCE": "a whole sentence on its own; write nothing else in that sentence", | |
| "CLAUSE": "after its subject and a verb, e.g. 'the price sits …'", | |
| "COMPACT": "after its subject and a verb, e.g. 'the price is …'", | |
| "PREDICATE": "directly after its subject; it carries its own verb", | |
| "NOUN_PHRASE": "as an object, e.g. 'the stock trades at …'"}[_mode] + ")", render_mode=_mode, | |
| # a whole-sentence rendering is for report templates; authors write clauses | |
| persona_visible=_mode != "FULL_SENTENCE")) | |
| # TrendRelationships: offered only when the earlier and later values are compatible (same metric, basis and perimeter) | |
| for _cid, _lbl in (("TREND_REVENUE_GROWTH", "revenue growth trend"), ("TREND_REVENUE", "revenue trend"), | |
| ("TREND_OPERATING_MARGIN", "operating margin trend"), ("TREND_EPS", "EPS trend")): | |
| CLAIMS.append(ClaimDef(_cid, f"relations.state.items.{_cid}", REL, f"{_lbl}: trailing → forward (complete relation)")) | |
| CLAIMS = [replace(c, slot="PREDICATE") if c.claim_id in ("RETURN_3M_PHRASE", "RETURN_12M_PHRASE") else c for c in CLAIMS] | |
| CLAIMS = [replace(c, unit_after="year") if c.claim_id in ("STORY_YEARS", "STORY_WACC_HOLD") else c for c in CLAIMS] | |
| _REQ = ("the price demands", "the price needs", "the price requires", "today's price needs", "today's price requires") | |
| READER_NAMES = { | |
| "BE_REVENUE_CAGR": ("break-even revenue growth", "break-even growth", "required growth", "required revenue growth") | |
| + tuple(f"growth {r}" for r in _REQ), | |
| "BE_REVENUE": ("break-even revenue", "required revenue", "required maturity revenue") + tuple(f"revenue {r}" for r in _REQ), | |
| "BE_MARGIN": ("break-even margin", "required margin", "required operating margin") + tuple(f"margin {r}" for r in _REQ)} | |
| READER_NAMES.update({ | |
| "FORECAST_EPS": ("our forecast", "our eps forecast", "engine forecast", "engine's forecast", "engine eps", "our estimate", | |
| "the forecast assumes", "our forecast assumes"), | |
| "CONSENSUS_EPS_NTM": ("consensus eps", "street eps", "consensus estimate"), | |
| "STORY_REVENUE_MATURITY": ("story's revenue at maturity", "maturity revenue", "story's maturity", "story revenue")}) | |
| CLAIMS = [replace(c, names=READER_NAMES[c.claim_id]) if c.claim_id in READER_NAMES else c for c in CLAIMS] | |
| BY_ID = {c.claim_id: c for c in CLAIMS} | |
| # the signed claim each PHRASE claim replaces for authors (the signed form stays for tables) | |
| PHRASE_OF = {"RETURN_3M": "RETURN_3M_PHRASE", "RETURN_12M": "RETURN_12M_PHRASE", | |
| "OWN_13F_HOLDERS_CHANGE": "OWN_13F_HOLDERS_CHANGE_PHRASE", "SCENARIO_BEAR_RETURN": "SCENARIO_BEAR_RETURN_PHRASE", | |
| "SCENARIO_NEUTRAL_RETURN": "SCENARIO_NEUTRAL_RETURN_PHRASE", "SCENARIO_BULL_RETURN": "SCENARIO_BULL_RETURN_PHRASE", | |
| "PM_RETURN": "PM_RETURN_PHRASE", "HEADLINE_RETURN": "HEADLINE_RETURN_PHRASE"} | |
| # reported and forecast income-statement lines (a statement noun in their clause must be the income statement) | |
| INCOME_STATEMENT_CLAIMS = ("REVENUE_TTM", "OPERATING_INCOME_TTM", "OPERATING_MARGIN_TTM", "NET_INCOME_TTM", "NET_MARGIN_TTM", | |
| "EPS_ADJUSTED_TTM", "EPS_GAAP_TTM", "FORECAST_REVENUE", "FORECAST_REVENUE_GROWTH", | |
| "FORECAST_OPERATING_MARGIN", "FORECAST_NET_MARGIN", "FORECAST_EPS", "FORECAST_EPS_GROWTH", | |
| "CONSENSUS_EPS_NTM", "CONSENSUS_EPS_FY1", "CONSENSUS_EPS_FY2", "CONSENSUS_REVENUE_FY1") | |
| # presentation claims that show only a relation's magnitude: report templates only, never offered to authors | |
| RELATION_PRESENTATION = ("PRICE_VS_INTRINSIC", "PRICE_VS_MECHANICAL", "PRICE_VS_COMPOSITE", "TARGET_VS_PRICE", | |
| "FORECAST_EPS_VS_CONSENSUS", "BE_REVENUE_CAGR") | |
| BY_ID = {c.claim_id: c for c in CLAIMS} | |
| class Claim: | |
| claim_id: str | |
| path: str | |
| stype: str | |
| label: str | |
| value: object | |
| rendered: str | None | |
| prohibited: tuple = () | |
| deferred: bool = False | |
| extra: dict = field(default_factory=dict) | |
| required_qualifiers: tuple = () | |
| def to_dict(self): | |
| return {"claim_id": self.claim_id, "path": self.path, "stype": self.stype, "label": self.label, | |
| "value": self.value, "rendered": self.rendered} | |
| def _resolve(state, path): | |
| v = state.get(path) | |
| if hasattr(v, "value") and hasattr(v, "metric_id"): | |
| v = v.value | |
| if isinstance(v, float) and v != v: # NaN is missing data, never a figure | |
| return None | |
| return v | |
| def _article(m): | |
| """'an' before a figure read with a vowel sound (8…, 11…, 18…, 80–89…).""" | |
| d = m.lstrip("+-$").split(".")[0].replace(",", "") | |
| return "an" if (d.startswith("8") or (len(d) == 2 and d in ("11", "18"))) else "a" | |
| def return_claim(value, phrase): | |
| """ReturnClaim: signed value, magnitude, direction and the phrase that carries the direction itself.""" | |
| if value is None or not phrase: | |
| return None | |
| up, down, zero, mst = phrase | |
| v = float(value) | |
| mag = abs(v) | |
| m = fmt(mst, mag) | |
| direction = "FLAT" if (fmt(mst, mag) in ("0.0%", "0") or mag == 0) else ("UP" if v > 0 else "DOWN") | |
| tpl = {"UP": up, "DOWN": down, "FLAT": zero}[direction] | |
| return {"value_signed": v, "magnitude": mag, "direction": direction, "render_mode": "phrase", | |
| "rendered": tpl.format(m=m, a=_article(m))} | |
| def _rel_value(v): | |
| from collections.abc import Mapping | |
| if not isinstance(v, Mapping): | |
| return None | |
| return {k: v.get(k) for k in ("claim_id", "lhs_claim_id", "rhs_claim_id", "relation_type", "magnitude", | |
| "magnitude_unit", "magnitude_definition", "lhs_value", "rhs_value", "earlier_value", | |
| "later_value")} | |
| def _guidance_value(v): | |
| from collections.abc import Mapping | |
| if not isinstance(v, Mapping): | |
| return None | |
| return {k: v.get(k) for k in ("concept", "quantity_type", "unit", "operator", "low", "high", "raw_low", "raw_high", | |
| "raw_unit", "accounting_basis", "period_label")} | |
| def claim_for(state, cdef): | |
| if cdef.label_path and state.get(cdef.label_path): | |
| cdef = replace(cdef, label=str(state.get(cdef.label_path))) | |
| if cdef.stype == REL: | |
| raw = state.get(cdef.path) | |
| v = _rel_value(raw) | |
| rendered = (((raw.get("renderings") or {}).get(cdef.render_mode) or (raw.get("rendered") if cdef.render_mode == "FULL" | |
| else None)) if v is not None else None) | |
| if rendered is None: | |
| v = None | |
| return Claim(cdef.claim_id, cdef.path, cdef.stype, cdef.label, v, rendered, cdef.prohibited, cdef.deferred, | |
| required_qualifiers=cdef.required_qualifiers) | |
| if cdef.stype == GUIDANCE: | |
| raw = state.get(cdef.path) | |
| v = _guidance_value(raw) | |
| rendered = render_guidance(raw) if v is not None else None | |
| return Claim(cdef.claim_id, cdef.path, cdef.stype, cdef.label, v, rendered, cdef.prohibited, cdef.deferred, | |
| required_qualifiers=cdef.required_qualifiers) | |
| v = _resolve(state, cdef.path) | |
| if cdef.stype == PHRASE: | |
| rc = return_claim(v, cdef.phrase) | |
| return Claim(cdef.claim_id, cdef.path, cdef.stype, cdef.label, v, rc["rendered"] if rc else None, cdef.prohibited, | |
| cdef.deferred, extra={"return_claim": rc} if rc else {}, required_qualifiers=cdef.required_qualifiers) | |
| if cdef.stype == TEXT and cdef.path.endswith("_usd_b") and v is not None: | |
| rendered = f"${float(v):,.0f}B" | |
| else: | |
| rendered = fmt(cdef.stype, v) if not (cdef.stype == TEXT and v is not None) else str(v) | |
| return Claim(cdef.claim_id, cdef.path, cdef.stype, cdef.label, v, rendered, cdef.prohibited, cdef.deferred, | |
| required_qualifiers=cdef.required_qualifiers) | |
| _GUIDANCE_PROHIBITED = ("consensus", "analyst", "our forecast", "engine forecast", "the model", "street") | |
| def guidance_claims(state): | |
| """GUIDANCE_n: one claim per verified company-guidance row (guidance.build_state), and nothing else. | |
| The only way prose may state or describe company guidance.""" | |
| from collections.abc import Mapping | |
| g = state.get("guidance.company") or {} | |
| if not isinstance(g, Mapping) or g.get("status") != "AVAILABLE": # sealed namespaces hold read-only mappings | |
| return [] | |
| # the token is the VALUE only ("335–360 bps"); its label and period are words the author writes, so a sentence | |
| # that names the metric does not repeat it ("… amortization guidance of [[GUIDANCE_6]]") | |
| return [ClaimDef(f"GUIDANCE_{i}", f"guidance.company.metrics.{i - 1}", GUIDANCE, | |
| f"company guidance value: {m.get('label')}, {m.get('period_label')} " | |
| f"({str(m.get('quantity_type') or '').lower().replace('_', ' ')})", _GUIDANCE_PROHIBITED) | |
| for i, m in enumerate(g.get("metrics") or [], 1)] | |
| def claim_def(state, cid): | |
| """The ClaimDef for a claim id: the static catalog, else a dynamic guidance claim.""" | |
| if cid in BY_ID: | |
| return BY_ID[cid] | |
| return next((c for c in guidance_claims(state) if c.claim_id == cid), None) | |
| # quantity families: two claims are candidates for comparison only within a family (or across related families) | |
| FAMILY = {"OPERATING_MARGIN": "margin", "NET_MARGIN": "margin", "GROSS_MARGIN": "margin", "TAX_RATE": "margin", | |
| "OPERATING_MARGIN_CHANGE": "margin_change", "GROSS_MARGIN_CHANGE": "margin_change", | |
| "REVENUE_GROWTH": "growth", "ORGANIC_REVENUE_GROWTH": "growth", "EPS_GROWTH": "growth", | |
| "REVENUE": "revenue", "EPS": "eps", "PE": "pe", "COST_OF_CAPITAL": "wacc", | |
| "PRICE": "value", "INTRINSIC_VALUE": "value", "MARKET_PRICING": "value", "TARGET_PRICE": "value", | |
| "BREAK_EVEN_REVENUE": "revenue", "BREAK_EVEN_OPERATING_MARGIN": "margin", "BREAK_EVEN_REVENUE_CAGR": "growth"} | |
| _RELATED = {frozenset({"margin", "margin_change"}): "a level and a change are different quantities"} | |
| def _claim_metric(state, cid): | |
| d = claim_def(state, cid) | |
| if d is None: | |
| return None | |
| m = state.get(d.path) | |
| if d.stype == GUIDANCE and m is not None: | |
| # a guidance row is a typed range: its identity (concept, quantity type, unit, period) is the claim | |
| from state import FY1, metric as _metric | |
| lo, hi = m.get("low"), m.get("high") | |
| return _metric(str(m.get("metric_id") or cid), m.get("concept"), ((lo + hi) / 2.0) if (lo is not None and hi is not None) | |
| else (lo if lo is not None else hi), FY1, quantity_type=m.get("quantity_type"), display_unit=m.get("unit"), | |
| unit="ratio" if m.get("unit") not in ("USD", "USD_MILLION", "USD_BILLION", "USD_PER_SHARE") else "USD", | |
| perimeter="company guidance", source="company guidance (8-K)") | |
| return m if hasattr(m, "economic_concept") else None | |
| def comparison(state, cid_a, cid_b): | |
| """(allowed, reasons) for comparing two claims, or None when they are not the same kind of quantity (then the | |
| sentence is not a like-for-like comparison and this layer has no opinion). Uses state.compatible().""" | |
| from state import compatible | |
| a, b = _claim_metric(state, cid_a), _claim_metric(state, cid_b) | |
| if a is None or b is None: | |
| return None | |
| fa, fb = FAMILY.get(a.economic_concept), FAMILY.get(b.economic_concept) | |
| if fa is None or fb is None: | |
| return None | |
| if fa != fb: | |
| why = _RELATED.get(frozenset({fa, fb})) | |
| return (False, [why]) if why else None | |
| from state import SCENARIO_BASE | |
| if fa == "value": | |
| ctype = "valuation" | |
| elif a.economic_concept in SCENARIO_BASE or b.economic_concept in SCENARIO_BASE: | |
| ctype = "scenario" if a.period_class == b.period_class else "path" | |
| elif "INDUSTRY" in (a.scope, b.scope) or "PEER" in (a.scope, b.scope): | |
| ctype = "benchmark" | |
| elif a.period_class != b.period_class: | |
| ctype = "trend" | |
| else: | |
| ctype = "level" | |
| ok, why = compatible(a, b, ctype) | |
| return ok, list(dict.fromkeys(why)) | |
| def pair_status(state, a, b, pairs=None): | |
| """None when two claims are not related quantities; ("RELATION", id) when an approved relation connects them; | |
| ("OK",) when they are compatible without one; ("INCOMPATIBLE", reasons) otherwise (compatible(), not words).""" | |
| ma, mb = _claim_metric(state, a), _claim_metric(state, b) | |
| if ma is None or mb is None: | |
| return None | |
| fa, fb = FAMILY.get(ma.economic_concept), FAMILY.get(mb.economic_concept) | |
| if fa is None or fb is None or (fa != fb and frozenset({fa, fb}) not in _RELATED): | |
| return None | |
| rel = (pairs or {}).get(frozenset({a, b})) | |
| if rel: | |
| return ("RELATION", rel) | |
| r = comparison(state, a, b) | |
| if r is None: | |
| return None | |
| return ("OK",) if r[0] else ("INCOMPATIBLE", r[1]) | |
| def comparison_permissions(state, cat): | |
| """[(a, b, reasons)] for every pair of offered claims of one family that may NOT be compared.""" | |
| ids = [c for c in cat if not cat[c].deferred] | |
| out = [] | |
| for i, a in enumerate(ids): | |
| for b in ids[i + 1:]: | |
| r = comparison(state, a, b) | |
| if r is not None and not r[0]: | |
| out.append((a, b, r[1])) | |
| return out | |
| # words a sentence uses to point at a counterpart it does not cite ("growth of [[X]] below trailing") | |
| _PERIOD_WORDS = {"TRAILING": ("trailing", "last year", "prior year", "past year", "historical", "historically", | |
| "a year ago", "last twelve months"), | |
| "FORWARD": ("forward", "next year", "next twelve months", "going forward")} | |
| _PERIMETER_WORDS = {"pre-separation": ("before the spin", "before the separation", "pre-spin", "pre-separation", | |
| "including the separated", "with the divested")} | |
| def implicit_counterparts(state, cat): | |
| """{claim: [(counterpart, reasons, reference words, trend)]} for every incomparable same-family pair. A sentence | |
| that cites one claim and points at the other by its period or perimeter ("below trailing"), or describes a trend | |
| in a growth rate whose historical counterpart is incomparable ("decelerating"), compares them implicitly.""" | |
| out = {} | |
| for a, b, why in comparison_permissions(state, cat): | |
| ma, mb = _claim_metric(state, a), _claim_metric(state, b) | |
| for x, mx, y, my in ((a, ma, b, mb), (b, mb, a, ma)): | |
| own = set(_PERIOD_WORDS.get(mx.period_class, ())) | set(_PERIMETER_WORDS.get(mx.perimeter, ())) | |
| words = tuple(w for w in _PERIOD_WORDS.get(my.period_class, ()) + _PERIMETER_WORDS.get(my.perimeter, ()) | |
| if w not in own) | |
| trend = FAMILY.get(mx.economic_concept) == "growth" and my.period_class == "TRAILING" and mx.period_class != "TRAILING" | |
| if words or trend: | |
| out.setdefault(x, []).append((y, why, words, trend)) | |
| return out | |
| def claim_descriptors(state, cat): | |
| """{claim: (family, period/perimeter words that describe it)}: a word that describes a CITED claim of the same | |
| family points at that claim, not at an uncited counterpart.""" | |
| out = {} | |
| for cid in cat: | |
| m = _claim_metric(state, cid) | |
| if m is not None and FAMILY.get(m.economic_concept): | |
| out[cid] = (FAMILY[m.economic_concept], | |
| tuple(_PERIOD_WORDS.get(m.period_class, ())) + tuple(_PERIMETER_WORDS.get(m.perimeter, ()))) | |
| return out | |
| _STYPE_KIND = {PCT: "rate", PCT_SIGNED: "rate", PP: "rate_change", BPS: "rate_change", CURRENCY: "per_share", | |
| CURRENCY_BIG: "money", MULTIPLE: "multiple"} | |
| _UNIT_KIND = {"PERCENT": "rate", "DECIMAL_FRACTION": "rate", "BASIS_POINTS": "rate_change", "PERCENTAGE_POINTS": "rate_change", | |
| "USD": "money", "USD_MILLION": "money", "USD_BILLION": "money", "USD_PER_SHARE": "per_share", "MULTIPLE": "multiple"} | |
| def claim_range(state, cid): | |
| """(low, high, kind) of a numeric claim in canonical units, or None. A guidance row is a range; every other claim | |
| is a point. Two claims can be ordered only when their kinds match (a rate against a rate, never a rate against a | |
| change in basis points).""" | |
| d = claim_def(state, cid) | |
| if d is None: | |
| return None | |
| if cid.startswith("GUIDANCE_"): | |
| try: | |
| m = ((state.get("guidance.company") or {}).get("metrics") or [])[int(cid.split("_")[1]) - 1] | |
| except (ValueError, IndexError): | |
| return None | |
| k = _UNIT_KIND.get(m.get("unit")) | |
| lo, hi = m.get("low"), m.get("high") | |
| return (lo, hi if hi is not None else lo, k) if (k and lo is not None) else None | |
| k = _STYPE_KIND.get(d.stype) | |
| v = state.value(d.path) if hasattr(state, "value") else None | |
| return (float(v), float(v), k) if (k and isinstance(v, (int, float))) else None | |
| def render_guidance(m, mode="value_only"): | |
| """One guidance row as text, formatted from its typed unit and quantity type (guidance.render).""" | |
| import guidance as _g | |
| return _g.render(m, mode) | |
| def guidance_basis_label(cat, cid): | |
| """'GAAP' / 'adjusted' when a guidance row has a sibling of the same concept on another basis with a different | |
| value (the basis then decides which figure is meant); None when the basis does not change the figure.""" | |
| from collections.abc import Mapping | |
| c0 = cat.get(cid) if str(cid or "").startswith("GUIDANCE_") else None # only a guidance row has a basis | |
| v = c0.value if c0 is not None and isinstance(c0.value, Mapping) else {} | |
| if not v.get("concept"): | |
| return None | |
| sib = {(x.get("accounting_basis"), x.get("low"), x.get("high")) for x in | |
| (c.value for k, c in cat.items() if k.startswith("GUIDANCE_") and isinstance(c.value, Mapping)) | |
| if x.get("concept") == v["concept"]} | |
| if len({(lo, hi) for _b, lo, hi in sib}) > 1 and len({b for b, _lo, _hi in sib}) > 1: | |
| return {"GAAP": "GAAP", "ADJUSTED": "adjusted"}.get(v.get("accounting_basis")) | |
| return None | |
| def build_catalog(state, include_deferred=True): | |
| """{claim_id: Claim} for every claim whose value exists in the state.""" | |
| cat = {} | |
| for c in list(CLAIMS) + guidance_claims(state): | |
| if c.deferred and not include_deferred: | |
| continue | |
| cl = claim_for(state, c) | |
| if cl.value is not None and cl.rendered is not None: | |
| cat[c.claim_id] = cl | |
| return cat | |
| def persona_comparison_text(state, cat): | |
| """The structural comparison permissions a persona receives (computed with compatible(), not asserted in prose).""" | |
| bad = comparison_permissions(state, cat) | |
| if not bad: | |
| return "" | |
| lines = ["COMPARISONS NOT ALLOWED (different concept, period basis or business perimeter; a sentence that compares " | |
| "them — vs, above, below, exceeds, from … to, decelerates, premium, discount — is removed):"] | |
| for a, b, why in bad[:30]: | |
| lines.append(f" [[{a}]] ✕ [[{b}]] — {'; '.join(why)[:120]}") | |
| return "\n".join(lines) | |
| def offered(state, cat, include_deferred=False): | |
| """The claim ids an LLM author may cite: facts, complete relations, phrase-rendered signed quantities and the | |
| evidence of models whose OWN reliability allows it. Relation magnitudes without their pair, signed returns | |
| without their direction and hidden models' claims are never offered.""" | |
| import evidence as _ev | |
| out = [] | |
| for cid, c in cat.items(): | |
| d = BY_ID.get(cid) or (claim_def(state, cid) if state is not None else None) | |
| if d is None or not d.persona_visible or (c.deferred and not include_deferred): | |
| continue | |
| if cid in RELATION_PRESENTATION or cid in PHRASE_OF: | |
| continue | |
| mid = _ev.claim_model(cid) | |
| if state is not None and mid and not _ev.model_visible(state, mid): | |
| continue | |
| out.append(cid) | |
| return out | |
| def persona_catalog_text(cat, state=None, include_deferred=False): | |
| """What an author sees, in three groups: FACT tokens (one quantity), RELATIONSHIP tokens (a complete typed | |
| comparison) and EVIDENCE tokens (a model's reliability, with the models it may be used about).""" | |
| import evidence as _ev | |
| ids = offered(state, cat, include_deferred) | |
| rels = [c for c in ids if cat[c].stype == REL] | |
| evid = [c for c in ids if c in _ev._EVIDENCE] | |
| facts = [c for c in ids if c not in rels and c not in evid] | |
| lines = ["NUMBER TOKENS — write every figure ONLY as its token. Never type a number, percentage, dollar amount or multiple " | |
| "yourself; a sentence that does is removed. Only the tokens listed below may be used.", | |
| "FACT tokens state one quantity. RELATIONSHIP tokens state a complete comparison — its subject, direction, size and " | |
| "the other side — as one clause: write it as a clause (e.g. \"Today, [[REL_PRICE_MARKET_COMPOSITE]].\") and never " | |
| "restate its direction or attach it to a different subject.", | |
| "Two figures of the same kind (two per-share values, two revenues, two margins, two growth rates, or a margin " | |
| "CHANGE and a margin LEVEL) may share a sentence ONLY through a relationship token that connects them; otherwise " | |
| "put them in separate sentences and do not compare them yourself.", | |
| "Whether today's price is more or less demanding (or optimistic) than the story may be stated ONLY by citing " | |
| "[[REL_EXPECTATIONS_STORY]], and only as it states.", | |
| "Each relationship has up to three forms of the SAME typed relation: [[REL_X]] is a complete clause — it must open " | |
| "the sentence or follow a comma, semicolon, colon or a conjunction (and, but, while, because, since, that), and be " | |
| "followed by punctuation or a conjunction; never write \"is [[REL_X]]\", \"per [[REL_X]]\" or \"[[REL_X]] shows\". " | |
| "Typed slots: [[REL_X__PREDICATE]] carries its own verb and follows its subject directly (\"the price " | |
| "[[REL_PRICE_MARKET_COMPOSITE__PREDICATE]]\"); [[REL_X__CLAUSE]] and [[REL_X__COMPACT]] follow the subject AND a verb " | |
| "(\"the price sits [[REL_PRICE_MARKET_COMPOSITE__COMPACT]]\"); [[REL_X__NOUN_PHRASE]] is an object (\"the stock trades " | |
| "at [[REL_PRICE_INTRINSIC__NOUN_PHRASE]]\"). Never write the other side again after the token. At most two complete " | |
| "relationship clauses per sentence.", | |
| "Premium, discount, above, below, cheap or expensive about the story value, the mechanical DCF or the market " | |
| "composite may be written only with that anchor's relationship token, and only in its direction.", | |
| "The break-even revenue and break-even margin are ALTERNATIVE solves (each holds the other constant): never put " | |
| "both in one sentence, and never say the price requires both revenue and margin, except through " | |
| "[[REL_EXPECTATIONS_STORY]].", | |
| "A segment count belongs to a perimeter: [[SEGMENT_COUNT_FILED]] is the business as filed (write it with 'before the " | |
| "separation' or 'as filed'); [[SEGMENT_COUNT_CONTINUING]] is the continuing company. Never type a segment count.", | |
| "Momentum, ownership, insider and quant evidence cannot establish a trend in the business (e.g. a price below the " | |
| "200-day average says nothing about revenue deceleration).", | |
| "A trend conclusion (accelerating, decelerating, improving, deteriorating, expanding, contracting) about revenue, " | |
| "growth, margins or earnings needs a TREND token; when none is listed there is no compatible earlier figure and no " | |
| "such trend may be asserted.", | |
| "A comma alone does not join a complete relationship clause to a phrase: write \"At [[PRICE]], [[REL_X]]\" or " | |
| "\"…, and [[REL_X]]\", never \"Buybacks at [[PRICE]], [[REL_X]], destroy value\" or \"…, which [[REL_X]]\". A " | |
| "relationship token already states its own figures: do not cite them again as separate tokens in that sentence.", | |
| "Whether a fundamental (growth, margins, earnings) is stronger or weaker than what the market prices in is a " | |
| "statement of expectations: only through [[REL_EXPECTATIONS_STORY]], and only in its direction.", | |
| "How the stock has performed (a weak year, a rally, a sell-off, underperformance) needs its signed return token " | |
| "in the same sentence ([[RETURN_12M_PHRASE]] for a year, [[RETURN_3M_PHRASE]] for three months).", | |
| "A quotation from the call keeps its speaker: an analyst's question is never something the company or management " | |
| "acknowledges; quote only words that are in the transcript.", | |
| "Keep each figure on its own financial statement: amortization, interest expense, tax rate and EPS are income-" | |
| "statement items; capex, buybacks and dividends are cash-flow items.", | |
| "[[RETURN_3M_PHRASE]] / [[RETURN_12M_PHRASE]] are predicates (\"down … over twelve months\"): write them after a " | |
| "verb or their subject (\"the shares are …\", \"with shares …\"), never after a bare preposition. A token whose " | |
| "value is a list (items joined by semicolons) ends its sentence.", | |
| "Never compare two different metric families (earnings vs revenue, margins vs revenue, cash flow vs earnings) " | |
| "\"faster\", \"slower\" or \"ahead of\" one another: no typed relation connects them.", | |
| "\"Close to fair value\", \"fairly valued\" or \"in line with value\" need a valuation relationship token whose " | |
| "gap is within 5%; otherwise state the gaps and do not call it close.", | |
| "A direction you give your own expected return (bearish, bullish, negative, positive) must match its sign.", | |
| "What management said on the call is stated only with its own words in quotation marks (at least three " | |
| "words, verbatim from a management answer); never paraphrase what an executive said or acknowledged.", | |
| "Verbal magnitudes are numbers: \"mid-single-digit\", \"high-teens\" or \"double-digit\" growth must fit a " | |
| "growth figure listed above for that metric. Never spell out a count (\"two adds\"): cite its token.", | |
| "Name every money figure and multiple by what it measures (\"revenue of …\", \"a forward P/E of …\").", | |
| "You may use [[OWN_RETURN]] for your own scenario return (it renders with its own direction)."] | |
| assess = ((state.get("relations.state") or {}).get("assessments") or {}) if state is not None else {} | |
| if assess: | |
| words = {"CONSERVATIVE": "may be called conservative, never aggressive", "AGGRESSIVE": "may be called aggressive, " | |
| "never conservative", "NEUTRAL": "is neither conservative nor aggressive", | |
| "NO_HAIRCUT": "applies no haircut: it is never conservative"} | |
| lines.append("ASSUMPTION ASSESSMENTS (conservative / aggressive may be said only as stated here; any other story " | |
| "assumption, or the assumptions as a group, takes neither word): " | |
| + "; ".join(f"[[{c}]] {words.get(a.get('assessment'), 'takes neither word')} ({a.get('basis')})" | |
| for c, a in assess.items())) | |
| interp = ((state.get("evidence.state") or {}).get("interpretation") or {}) if state is not None else {} | |
| for fam, d in interp.items(): | |
| if d.get("permission") == "DESCRIPTIVE_ONLY": | |
| lines.append(f"{fam} evidence is DESCRIPTIVE ONLY: state the positions, trades or counts; never characterise them " | |
| "(weakening, supportive, constructive, fading, a floor, a signal …) or draw a direction from them.") | |
| q = lambda cid: (BY_ID.get(cid).required_qualifiers if cid in BY_ID else ()) | |
| lines.append("FACT TOKENS (token = value — meaning):") | |
| for cid in facts: | |
| lines.append(f" [[{cid}]] = {cat[cid].rendered} — {cat[cid].label}" | |
| + (f" (write it with one of: {', '.join(q(cid)[:3])})" if q(cid) else "")) | |
| if rels: | |
| lines.append("RELATIONSHIP TOKENS (token = what it renders; FULL is a clause, __CLAUSE / __COMPACT follow their subject):") | |
| lines += [f" [[{cid}]] = \"{cat[cid].rendered}\"" for cid in rels] | |
| if evid: | |
| lines.append("EVIDENCE TOKENS (usable ONLY about the model named; never about another model):") | |
| for cid in evid: | |
| src, tg = _ev.evidence_targets(state, cid) if state is not None else (_ev._EVIDENCE[cid][0], [_ev._EVIDENCE[cid][0]]) | |
| lines.append(f" [[{cid}]] = {cat[cid].rendered} — {cat[cid].label}; source model {src}; may describe only: " | |
| f"{', '.join(tg)}") | |
| return "\n".join(lines) | |
| def path_claim(state, path, stype, label): | |
| """An ad-hoc claim for a renderer number that is not in the named catalog (tables, story path). | |
| Its id is derived from the path, so it still resolves structurally to one state location.""" | |
| v = _resolve(state, path) | |
| cid = "P:" + path | |
| return Claim(cid, path, stype, label, v, fmt(stype, v) if v is not None else None) | |
| def is_full_clause(cid, cat=None): | |
| """A FULL relationship, a trend relationship or a clause-rendered phrase: a complete clause, never a noun phrase.""" | |
| d = BY_ID.get(cid) | |
| if d is None: | |
| return False | |
| return (d.stype == REL and d.render_mode == "FULL") or d.clause | |