"""历史冻结预测按同提前期评估;合成归档,不从当前报告重建历史。""" import json from datetime import date, timedelta from decimal import Decimal as D, InvalidOperation def finite(value): try: number = D(str(value)) except (InvalidOperation, ValueError, TypeError) as exc: raise ValueError("invalid_numeric_input") from exc if not number.is_finite(): raise ValueError("nonfinite_eps") return number actuals = {2024: {"eps": "1.00", "release": "2025-03-01"}, 2025: {"eps": "1.20", "release": "2026-03-01"}} forecasts = [] for year, brokers in {2024: {"A": {180: "1.20", 30: "1.05"}, "B": {180: "0.90", 30: "0.98"}}, 2025: {"A": {180: "1.00", 30: "1.15"}, "B": {30: "1.18"}}}.items(): release = date.fromisoformat(actuals[year]["release"]) for broker, horizons in brokers.items(): for horizon, eps in horizons.items(): forecasts.append({"broker": broker, "year": year, "eps": eps, "frozen_at": str(release-timedelta(days=horizon)), "source_id": f"archive:{broker}:{year}:{horizon}", "basis": "diluted", "currency": "CNY"}) def select(rows, broker, year, horizon): cutoff = date.fromisoformat(actuals[year]["release"])-timedelta(days=horizon) eligible = [r for r in rows if r["broker"] == broker and r["year"] == year and cutoff-timedelta(days=3) <= date.fromisoformat(r["frozen_at"]) <= cutoff] if not eligible: return None latest = max(r["frozen_at"] for r in eligible) same_day = [r for r in eligible if r["frozen_at"] == latest] if any(r["basis"] != "diluted" or r["currency"] != "CNY" for r in same_day): raise ValueError("incompatible_eps_basis") values = {finite(r["eps"]) for r in same_day} if len(values) != 1: raise ValueError("conflicting_frozen_forecast") return sorted(same_day, key=lambda r: r["source_id"])[0] def error(forecast, actual): f, a = finite(forecast), finite(actual) return {"absolute_difference": f-a, "relative": (f-a)/a if a > D("0.05") else None} def evaluate(rows, horizon): output = {} for broker in ["A", "B"]: errors, missing = [], [] for year in actuals: selected = select(rows, broker, year, horizon) if selected is None: missing.append(year) else: errors.append(error(selected["eps"], actuals[year]["eps"])["absolute_difference"]) output[broker] = {"n": len(errors), "eligible_actuals": len(actuals), "missing_years": missing, "mae": str(sum(map(abs, errors))/len(errors)) if errors else None, "bias": str(sum(errors)/len(errors)) if errors else None} return output long = evaluate(forecasts, 180) short = evaluate(forecasts, 30) assert long["A"]["mae"] == "0.20" and long["A"]["bias"] == "0.00" assert long["B"]["n"] == 1 and long["B"]["missing_years"] == [2025] assert short["A"]["mae"] == "0.05" and short["B"]["mae"] == "0.02" assert select(forecasts, "B", 2025, 180) is None near_zero = error("0.03", "0.01") assert near_zero == {"absolute_difference": D("0.02"), "relative": None} conflict = [{**forecasts[0], "eps": "9.00"}, *forecasts] for rows in [conflict, list(reversed(conflict))]: try: evaluate(rows, 180) except ValueError as exc: assert str(exc) == "conflicting_frozen_forecast" else: raise AssertionError("同日冲突必须拒绝") for token in ["NaN", "Infinity", "-Infinity"]: for pair in [(token, "1"), ("1", token)]: try: error(*pair) except ValueError: pass else: raise AssertionError("非有限EPS必须阻断") print(json.dumps({"180_days": long, "30_days": short, "near_zero": near_zero}, default=str, sort_keys=True))