"""外供同行、同估值日与EPS口径核对后计算前瞻PE。""" import json from decimal import Decimal as D from statistics import median from datetime import datetime def row(symbol, price, eps, **changes): value = {"symbol": symbol, "price": D(price), "eps": D(eps) if eps is not None else None, "price_date": "2026-10-26", "year": 2027, "basis": "diluted", "price_currency": "CNY", "eps_currency": "CNY", "business_group": "supplied-software-peers", "source": "synthetic-"+symbol} value["fiscal_end"] = str(changes.get("year", 2027))+"-12-31" value["forecast_available"] = "2026-10-20T10:00:00+08:00" value.update(changes) return value rows = [row("AAA", "24", "1.2"), row("BBB", "30", "2"), row("CCC", "40", "2"), row("DDD", "50", "2"), row("EEE", "20", "-1"), row("FFF", "30", "2", year=2028), row("GGG", "30", "2", eps_currency="HKD"), row("HHH", "30", "2", basis="basic"), row("III", "30", "2", price_date="2026-10-23"), row("JJJ", "30", "2", business_group="utility"), row("KKK", "30", None), row("LLL", "30", "2", forecast_available="2026-10-27T10:00:00+08:00")] output = [] for item in rows: reasons = [] if item["year"] != 2027: reasons.append("wrong_year") if item["fiscal_end"] != "2027-12-31": reasons.append("wrong_fiscal_end") if item["price_date"] != "2026-10-26": reasons.append("wrong_price_date") if item["basis"] != "diluted": reasons.append("wrong_eps_basis") if item["price_currency"] != item["eps_currency"]: reasons.append("currency_mismatch") if item["business_group"] != "supplied-software-peers": reasons.append("different_business") if datetime.fromisoformat(item["forecast_available"]) > datetime.fromisoformat("2026-10-26T15:00:00+08:00"): reasons.append("future_forecast") if not item["price"].is_finite() or item["price"] <= 0: reasons.append("invalid_price") if item["eps"] is None: reasons.append("missing_eps") elif not item["eps"].is_finite(): reasons.append("nonfinite_eps") elif item["eps"] <= 0: reasons.append("nonpositive_eps") pe = item["price"]/item["eps"] if not reasons else None output.append({"symbol": item["symbol"], "pe": str(pe) if pe is not None else None, "reasons": reasons, "source": item["source"]}) eligible_peers = [D(item["pe"]) for item in output if item["symbol"] != "AAA" and item["pe"] is not None] peer_median = median(eligible_peers) target_pe = D(output[0]["pe"]) relative = target_pe/peer_median-1 assert eligible_peers == [D("15"), D("20"), D("25")] and peer_median == D("20") assert target_pe == D("20") and relative == 0 assert all(item["pe"] is None and item["reasons"] for item in output[4:]) assert output[4]["reasons"] == ["nonpositive_eps"] assert len(output) == len(rows) == 12 assert output[-1]["reasons"] == ["future_forecast"] ordered_peers = sorted([r for r in output if r["symbol"] != "AAA" and r["pe"] is not None], key=lambda r: (D(r["pe"]), r["symbol"])) print(json.dumps({"rows": output, "target_pe": str(target_pe), "peer_median": str(peer_median), "target_relative_to_peer_median": str(relative), "eligible_peer_symbols": [r["symbol"] for r in ordered_peers]}, sort_keys=True))