← Files Equity CouncilARCHIVED FILE

scripts/scenario_rank.py

17.8 KB · Oct 2, 2026 · 00:34 UTC

↓ Download file

#!/usr/bin/env python3
"""Validate and compare user-authored equity scenarios; no empirical forecasts."""

import argparse
from datetime import date
from decimal import Decimal, localcontext
import json
import math
from pathlib import Path
import sys


class ValidationError(ValueError):
    """An input does not satisfy the scenario contract."""


def require(condition, message):
    if not condition:
        raise ValidationError(message)


def mapping(value, path):
    require(isinstance(value, dict), f"{path}: expected object")
    return value


def allowed_keys(value, expected, path):
    unknown = set(value) - set(expected)
    require(not unknown, f"{path}: unknown field(s): {', '.join(sorted(unknown))}")


def field(obj, key, path):
    require(key in obj, f"{path}.{key}: required")
    return obj[key]


def string(value, path):
    require(isinstance(value, str) and bool(value.strip()), f"{path}: expected nonempty string")
    return value


def number(value, path, minimum=None, maximum=None):
    require(type(value) in (int, float), f"{path}: expected number, not boolean")
    try:
        value = float(value)
    except (OverflowError, ValueError):
        raise ValidationError(f"{path}: expected finite number") from None
    require(math.isfinite(value), f"{path}: expected finite number")
    require(minimum is None or value >= minimum, f"{path}: below minimum {minimum}")
    require(maximum is None or value <= maximum, f"{path}: above maximum {maximum}")
    return value


def sequence(value, path, nonempty=True):
    require(isinstance(value, list), f"{path}: expected array")
    require(not nonempty or bool(value), f"{path}: must not be empty")
    return value


def finite_result(value, path):
    require(math.isfinite(value), f"{path}: arithmetic overflow; rescale inputs")
    return value


def decimal(value):
    """Preserve the supplied decimal value for event and policy boundaries."""
    return Decimal(str(value))


def probability_total(rows, condition=None):
    return float(sum((decimal(row["probability"]) for row in rows
                      if condition is None or row[condition]), Decimal(0)))


def economic_sensitivity(rows):
    """Exact decimal comparison of validated finite-float scenario inputs.

    Coalesce equivalent payoffs before weighting so splitting a state cannot
    manufacture sensitivity. Zero-probability states carry no economic mass.
    Use terminal cash rather than dividing by a common entry price.
    """
    with localcontext() as context:
        context.prec = 1100
        distribution = {}
        success = Decimal(0)
        severe_loss = Decimal(0)
        for row in rows:
            probability = decimal(row["probability"])
            if probability == 0:
                continue
            payoff = decimal(row["terminal_price"]) + decimal(row["cash_distributions"])
            key = (payoff, decimal(row["benchmark_wealth_multiple"]))
            distribution[key] = distribution.get(key, Decimal(0)) + probability
            if row["success"]:
                success += probability
            if row["severe_loss"]:
                severe_loss += probability
        signature = tuple(sorted((payoff, benchmark, mass)
                                 for (payoff, benchmark), mass in distribution.items()))
        expected_cash = sum((payoff * mass for payoff, _, mass in signature), Decimal(0))
        return signature, (expected_cash, success, severe_loss)


def annualize(wealth, horizon, path):
    if wealth == 0:
        return -1.0
    try:
        return finite_result(math.expm1(math.log(wealth) / horizon), path)
    except OverflowError:
        raise ValidationError(f"{path}: arithmetic overflow; rescale inputs") from None


def calculate_set(raw, path, price, horizon, threshold):
    mapping(raw, path)
    allowed_keys(raw, ("name", "scenarios"), path)
    name = string(field(raw, "name", path), f"{path}.name")
    rows = sequence(field(raw, "scenarios", path), f"{path}.scenarios")
    seen = set()
    metrics = []
    for index, raw_scenario in enumerate(rows):
        sp = f"{path}.scenarios[{index}]"
        row = mapping(raw_scenario, sp)
        allowed_keys(row, ("name", "probability", "terminal_price", "cash_distributions", "benchmark_wealth_multiple"), sp)
        scenario_name = string(field(row, "name", sp), f"{sp}.name")
        require(scenario_name not in seen, f"{sp}.name: duplicate scenario name")
        seen.add(scenario_name)
        probability = number(field(row, "probability", sp), f"{sp}.probability", 0, 1)
        terminal = number(field(row, "terminal_price", sp), f"{sp}.terminal_price", 0)
        cash = number(field(row, "cash_distributions", sp), f"{sp}.cash_distributions", 0)
        benchmark = number(field(row, "benchmark_wealth_multiple", sp), f"{sp}.benchmark_wealth_multiple", 0)
        wealth = finite_result((terminal + cash) / price, f"{sp}.wealth_multiple")
        total_return = wealth - 1.0
        terminal_cash = decimal(terminal) + decimal(cash)
        entry_cash = decimal(price)
        metrics.append({
            "name": scenario_name, "probability": probability,
            "terminal_price": terminal, "cash_distributions": cash,
            "benchmark_wealth_multiple": benchmark,
            "wealth_multiple": wealth, "total_return": total_return,
            "scenario_cagr": annualize(wealth, horizon, f"{sp}.scenario_cagr"),
            "success": terminal_cash > entry_cash and terminal_cash > entry_cash * decimal(benchmark),
            "severe_loss": terminal_cash <= entry_cash * (Decimal(1) - decimal(threshold)),
            "terminal_loss": terminal_cash < entry_cash,
        })
    probability_sum = probability_total(metrics)
    require(math.isclose(probability_sum, 1.0, rel_tol=0, abs_tol=1e-12),
            f"{path}: probabilities must sum to 1 (got {probability_sum!r})")
    expected_wealth = finite_result(math.fsum(row["probability"] * row["wealth_multiple"] for row in metrics), path)
    result = {
        "name": name, "probability_sum": probability_sum,
        "scenarios": metrics, "expected_wealth_multiple": expected_wealth,
        "expected_total_return": expected_wealth - 1.0,
        "annualized_expected_wealth": annualize(expected_wealth, horizon, path),
        "probability_weighted_scenario_cagr": math.fsum(row["probability"] * row["scenario_cagr"] for row in metrics),
        "success_probability": probability_total(metrics, "success"),
        "severe_loss_probability": probability_total(metrics, "severe_loss"),
        "terminal_loss_probability": probability_total(metrics, "terminal_loss"),
        "central_bull_upside_total_return": max(row["total_return"] for row in metrics if row["probability"] > 0),
    }
    signature, comparison_metrics = economic_sensitivity(metrics)
    return result, signature, comparison_metrics


def dominates(left, right):
    comparisons = (
        left["robust_annualized_expected_wealth"] >= right["robust_annualized_expected_wealth"],
        left["robust_success_probability"] >= right["robust_success_probability"],
        left["robust_severe_loss_probability"] <= right["robust_severe_loss_probability"],
    )
    strict = (
        left["robust_annualized_expected_wealth"] > right["robust_annualized_expected_wealth"]
        or left["robust_success_probability"] > right["robust_success_probability"]
        or left["robust_severe_loss_probability"] < right["robust_severe_loss_probability"]
    )
    return all(comparisons) and strict


def analyze(data):
    root = mapping(data, "input")
    allowed_keys(root, ("schema_version", "horizon_years", "currency", "severe_loss_threshold", "policy", "companies"), "input")
    version = field(root, "schema_version", "input")
    require(type(version) is int and version == 1, "schema_version: must be integer 1")
    horizon = number(field(root, "horizon_years", "input"), "horizon_years", 0)
    require(horizon > 0, "horizon_years: must be greater than zero")
    currency = string(field(root, "currency", "input"), "currency")
    threshold = number(field(root, "severe_loss_threshold", "input"), "severe_loss_threshold", 0, 1)
    require(threshold > 0, "severe_loss_threshold: must be greater than zero")
    policy = mapping(field(root, "policy", "input"), "policy")
    allowed_keys(policy, ("min_success_probability", "max_severe_loss_probability"), "policy")
    minimum = number(field(policy, "min_success_probability", "policy"), "policy.min_success_probability", 0, 1)
    maximum = number(field(policy, "max_severe_loss_probability", "policy"), "policy.max_severe_loss_probability", 0, 1)
    companies = sequence(field(root, "companies", "input"), "companies")
    ids = set()
    results = []
    for index, raw in enumerate(companies):
        path = f"companies[{index}]"
        company = mapping(raw, path)
        allowed_keys(company, ("id", "name", "currency", "price", "evidence_eligible", "eligibility_notes", "scenario_sets"), path)
        identifier = string(field(company, "id", path), f"{path}.id")
        require(identifier not in ids, f"{path}.id: duplicate company ID")
        ids.add(identifier)
        name = string(field(company, "name", path), f"{path}.name")
        company_currency = string(field(company, "currency", path), f"{path}.currency")
        require(company_currency == currency, f"{path}.currency: must match run currency {currency}")
        price_data = mapping(field(company, "price", path), f"{path}.price")
        allowed_keys(price_data, ("value", "as_of", "source_id"), f"{path}.price")
        price = number(field(price_data, "value", f"{path}.price"), f"{path}.price.value", 0)
        require(price > 0, f"{path}.price.value: must be greater than zero")
        as_of = string(field(price_data, "as_of", f"{path}.price"), f"{path}.price.as_of")
        try:
            parsed_date = date.fromisoformat(as_of)
        except ValueError:
            raise ValidationError(f"{path}.price.as_of: expected YYYY-MM-DD") from None
        require(parsed_date.isoformat() == as_of, f"{path}.price.as_of: expected YYYY-MM-DD")
        source_id = string(field(price_data, "source_id", f"{path}.price"), f"{path}.price.source_id")
        evidence = field(company, "evidence_eligible", path)
        require(type(evidence) is bool, f"{path}.evidence_eligible: expected boolean")
        notes = sequence(field(company, "eligibility_notes", path), f"{path}.eligibility_notes")
        for note_index, note in enumerate(notes):
            string(note, f"{path}.eligibility_notes[{note_index}]")
        sets = sequence(field(company, "scenario_sets", path), f"{path}.scenario_sets")
        set_names = set()
        calculated = []
        signatures = {}
        sensitivity_metrics = {}
        for set_index, raw_set in enumerate(sets):
            result, signature, comparison_metrics = calculate_set(raw_set, f"{path}.scenario_sets[{set_index}]", price, horizon, threshold)
            require(result["name"] not in set_names, f"{path}.scenario_sets: duplicate set name")
            set_names.add(result["name"])
            signatures[result["name"]] = signature
            sensitivity_metrics[result["name"]] = comparison_metrics
            calculated.append(result)
        require("central" in set_names, f"{path}.scenario_sets: central set is required")
        central = next(row for row in calculated if row["name"] == "central")
        distinct_sensitivity = any(signature != signatures["central"] for key, signature in signatures.items() if key != "central")
        central_cash, central_success, central_loss = sensitivity_metrics["central"]
        adverse_sensitivity = any(
            cash < central_cash or success < central_success or loss > central_loss
            for key, (cash, success, loss) in sensitivity_metrics.items() if key != "central")
        robust_return = min(row["annualized_expected_wealth"] for row in calculated)
        robust_success = min(row["success_probability"] for row in calculated)
        robust_loss = max(row["severe_loss_probability"] for row in calculated)
        reasons = []
        if not evidence:
            reasons.append("Evidence eligibility was not attested.")
        if not distinct_sensitivity:
            reasons.append("At least one numerically distinct sensitivity set is required for ranking.")
        if not adverse_sensitivity:
            reasons.append("At least one adverse sensitivity must lower expected terminal wealth or success probability, or increase severe-loss probability, versus central.")
        if robust_success < minimum:
            reasons.append("Robust success probability is below policy minimum.")
        if robust_loss > maximum:
            reasons.append("Robust severe-loss probability exceeds policy maximum.")
        if robust_return <= 0:
            reasons.append("Robust annualized expected wealth return is not positive.")
        results.append({
            "id": identifier, "name": name, "currency": currency,
            "price": {"value": price, "as_of": as_of, "source_id": source_id},
            "evidence_eligible": evidence, "eligibility_notes": notes,
            "has_distinct_sensitivity": distinct_sensitivity,
            "has_adverse_sensitivity": adverse_sensitivity,
            "scenario_sets": calculated, "central": central,
            "robust_annualized_expected_wealth": robust_return,
            "robust_success_probability": robust_success,
            "robust_severe_loss_probability": robust_loss,
            "eligible": not reasons, "watchlist_reasons": reasons,
            "pareto_frontier": False, "dominated_by": [], "rank": None,
        })
    eligible = [row for row in results if row["eligible"]]
    for row in eligible:
        row["dominated_by"] = sorted(other["id"] for other in eligible if dominates(other, row))
        row["pareto_frontier"] = not row["dominated_by"]
    ordered = sorted(eligible, key=lambda row: (
        not row["pareto_frontier"], -row["robust_annualized_expected_wealth"],
        -row["robust_success_probability"], row["robust_severe_loss_probability"], row["id"]))
    for rank, row in enumerate(ordered, 1):
        row["rank"] = rank
    return {
        "schema_version": 1, "horizon_years": horizon, "currency": currency,
        "severe_loss_threshold": threshold,
        "policy": {"min_success_probability": minimum, "max_severe_loss_probability": maximum},
        "caveats": [
            "Scenario probabilities are user-supplied assumptions, not verified empirical success estimates or guarantees.",
            "Evidence eligibility is a human attestation; this calculator does not verify sources or evidence.",
            "Terminal prices must already reflect dilution; no dilution adjustment is performed.",
            "Cash distributions are added without reinvestment. Reinvestment requires a separately documented alternative model outside this calculator contract.",
            "The adverse-sensitivity gate checks modeled metric direction only; humans must assess economic plausibility, stress magnitude, and comparable stress coverage.",
            "Success requires positive total return AND strict outperformance of the same-state benchmark.",
            "Severe loss is terminal total return at or below the loss threshold, not path drawdown or permanent impairment.",
            "Robust extrema can come from different sensitivity sets; they are not a single joint forecast.",
            "Policy gates use unrounded metrics; event boundaries and probability addition use decimal arithmetic. Probability sums permit only a 1e-12 tolerance and are not normalized.",
            "Rankings compare only supplied companies and scenarios; no ranking establishes the best investment in a market.",
        ],
        "companies": results,
        "ranked_ids": [row["id"] for row in ordered],
        "success_first_eligible_ids": [row["id"] for row in sorted(eligible, key=lambda row: (
            -row["robust_success_probability"], -row["robust_annualized_expected_wealth"],
            row["robust_severe_loss_probability"], row["id"]))],
        "central_bull_upside_eligible_ids": [row["id"] for row in sorted(eligible, key=lambda row: (
            -row["central"]["central_bull_upside_total_return"], row["id"]))],
        "alternative_ordering_note": "Eligible companies only. Bull upside is the highest positive-probability central scenario total return, not its likelihood or an expected return.",
        "watchlist_ids": sorted(row["id"] for row in results if not row["eligible"]),
    }


def reject_constant(value):
    raise ValidationError(f"Nonfinite JSON constant is not allowed: {value}")


def unique_object(pairs):
    result = {}
    for key, value in pairs:
        require(key not in result, f"Duplicate JSON object key: {key}")
        result[key] = value
    return result


def main(argv=None):
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("input", type=Path, help="UTF-8 scenario input JSON")
    parser.add_argument("--output", type=Path, help="Write UTF-8 result JSON (otherwise stdout)")
    args = parser.parse_args(argv)
    try:
        if args.output:
            same_path = args.input.resolve() == args.output.resolve()
            same_file = args.input.exists() and args.output.exists() and args.input.samefile(args.output)
            require(not (same_path or same_file), "Output must not overwrite input (including links to the same file).")
        data = json.loads(args.input.read_text(encoding="utf-8-sig"), parse_constant=reject_constant, object_pairs_hook=unique_object)
        output = json.dumps(analyze(data), ensure_ascii=False, indent=2, allow_nan=False) + "\n"
        if args.output:
            args.output.write_text(output, encoding="utf-8")
        else:
            if hasattr(sys.stdout, "reconfigure"):
                sys.stdout.reconfigure(encoding="utf-8")
            sys.stdout.write(output)
        return 0
    except (ValidationError, ValueError, OSError, OverflowError) as error:
        print(f"error: {error}", file=sys.stderr)
        return 2


if __name__ == "__main__":
    raise SystemExit(main())

SHA-256: 84f23a5cf0b1819339d489436c04208f58be008da9e980e749939f90b0f972bd