"""Figures used in five Prediction Market Math articles (Prediction Market Math).

Articles: whale-tracking-copy-trading-alpha, market-efficiency-calibration, kelly-criterion-binary,
implied-probability-pricing, cross-market-arbitrage.

Input:  polymarket-resolved-markets-2023-2026.csv (the same 3,236 resolved Polymarket markets as
        polymarket_calibration_analysis.py).
Output: printed JSON and pmm_article_figures.json

Notes
- No randomness. Data figures come from the CSV; Kelly and fee figures come from closed-form formulas.
- ROI for buying No at Yes price p: ((1 - outcome) - (1 - p)) / (1 - p) per market, fees ignored.
  Prices are the last hourly trade 24 hours before close, not an executable ask.
- Kelly drawdown probabilities use the standard continuous-time approximation (log wealth as Brownian motion).
Run: python pmm_article_figures.py
"""
import csv, json, math, os, statistics

HERE = os.path.dirname(os.path.abspath(__file__))
DATA = os.path.join(HERE, "polymarket-resolved-markets-2023-2026.csv")
FADE_BANDS = [(0.0, 0.01), (0.01, 0.03), (0.03, 0.05), (0.05, 0.10), (0.0, 0.10)]
BINS = [(i / 10, (i + 1) / 10) for i in range(10)]


def load():
    with open(DATA, encoding="utf-8") as f:
        return list(csv.DictReader(f))


def pairs(rows, col):
    return [(min(max(float(r[col]), 0.0), 1.0), int(r["outcome_yes"])) for r in rows if r[col] not in ("", None)]


def ci95(values):
    m = statistics.fmean(values); se = statistics.pstdev(values) / math.sqrt(len(values))
    return [round((m - 1.96 * se) * 100, 2), round((m + 1.96 * se) * 100, 2)]


def fade(rows):
    out = []
    for lo, hi in FADE_BANDS:
        b = [(p, y) for p, y in pairs(rows, "yes_price_24h") if lo <= p < hi]
        no_roi = [((1 - y) - (1 - p)) / (1 - p) for p, y in b]
        mean_p = statistics.fmean(p for p, _ in b)
        out.append({"yes_price_cents": [round(lo * 100), round(hi * 100)], "markets": len(b),
                    "mean_yes_price_pct": round(mean_p * 100, 2), "resolved_yes": sum(y for _, y in b),
                    "resolved_yes_pct": round(statistics.fmean(y for _, y in b) * 100, 2),
                    "buy_no_roi_pct": round(statistics.fmean(no_roi) * 100, 2), "buy_no_roi_ci95_pct": ci95(no_roi),
                    "wins_erased_by_one_loss_at_mean_price": round((1 - mean_p) / mean_p, 1)})
    return out


def calibration(rows, col):
    pr = pairs(rows, col); n = len(pr); table, rel = [], 0.0
    for lo, hi in BINS:
        b = [(p, y) for p, y in pr if lo <= p < hi or (hi == 1.0 and p == 1.0)]
        mp, of = statistics.fmean(p for p, _ in b), statistics.fmean(y for _, y in b)
        contrib = len(b) / n * (mp - of) ** 2; rel += contrib
        table.append({"price_band": f"{lo:.1f}-{hi:.1f}", "markets": len(b), "mean_price": round(mp, 4),
                      "resolved_yes": round(of, 4), "error_pp": round((mp - of) * 100, 2),
                      "reliability_contribution": round(contrib, 5)})
    extremes = sum(1 for p, _ in pr if p < 0.1 or p >= 0.9)
    return {"markets": n, "reliability": round(rel, 4), "extremes_below_10c_or_from_90c": extremes,
            "extremes_pct": round(extremes / n * 100, 1), "bands": table}


def brier(rows, col):
    return round(statistics.fmean((p - y) ** 2 for p, y in pairs(rows, col)), 4)


def growth(p, price, f):
    b = (1 - price) / price
    return p * math.log(1 + f * b) + (1 - p) * math.log(1 - f)


def kelly_table():
    rows = []
    for price, p in [(0.10, 0.18), (0.25, 0.35), (0.40, 0.50), (0.50, 0.60), (0.65, 0.75), (0.80, 0.88), (0.90, 0.95)]:
        f = (p - price) / (1 - price)
        g = [growth(p, price, f / k) for k in (1, 2, 4)]
        lo, hi = f * 1.0001, 1 - 1e-9  # bisection: stake at which growth falls back to zero
        for _ in range(200):
            mid = (lo + hi) / 2
            lo, hi = (mid, hi) if growth(p, price, mid) > 0 else (lo, mid)
        rows.append({"price": price, "p": p, "full_kelly_pct": round(f * 100, 2),
                     "growth_full_pct": round(g[0] * 100, 2), "growth_half_pct": round(g[1] * 100, 2),
                     "growth_quarter_pct": round(g[2] * 100, 2),
                     "half_share_of_full_pct": round(g[1] / g[0] * 100, 1), "quarter_share_of_full_pct": round(g[2] / g[0] * 100, 1),
                     "zero_growth_stake_multiple_of_kelly": round(lo / f, 2)})
    return rows


def kelly_drawdown():
    out = {}
    for c in (1.0, 0.5, 0.25):
        theta = (2 - c) / c  # 2 * drift / variance of log wealth at c times Kelly
        halve_before_double = (2 ** -theta - 1) / (2 ** -theta - 2 ** theta)
        out[str(c)] = {"growth_share_of_max_pct": round(c * (2 - c) * 100, 2), "variance_share_pct": round(c * c * 100, 2),
                       "p_halve_before_double_pct": round(halve_before_double * 100, 2),
                       "p_ever_halve_pct": round(0.5 ** theta * 100, 2)}
    return out


def arbitrage_after_fees(pa, pb, ra, rb):
    fee = ra * pa * (1 - pa) + rb * pb * (1 - pb)  # taker fee per contract: rate * p * (1 - p)
    return {"price_a": pa, "price_b": pb, "fee_per_pair": round(fee, 4), "cost_per_pair": round(pa + pb + fee, 4),
            "roi_gross_pct": round((1 / (pa + pb) - 1) * 100, 2), "roi_net_pct": round((1 / (pa + pb + fee) - 1) * 100, 2)}


def arbitrage_fee_examples():
    rows = [(0.45, 0.48), (0.52, 0.43), (0.60, 0.36), (0.30, 0.67), (0.75, 0.23), (0.40, 0.59), (0.50, 0.50)]
    return {"table_rows_both_0.07": [arbitrage_after_fees(a, b, 0.07, 0.07) for a, b in rows],
            "first_row_0.04_and_0.07": arbitrage_after_fees(0.45, 0.48, 0.04, 0.07)}


def main():
    rows = load()
    single = [r for r in rows if r["neg_risk"] in ("False", "false", "0", "")]
    low = [r for r in rows if r["yes_price_24h"] not in ("", None) and float(r["yes_price_24h"]) < 0.1]
    both = [r for r in rows if r["yes_price_24h"] not in ("", None) and r["yes_price_7d"] not in ("", None)]
    res = {"source": "polymarket-resolved-markets-2023-2026.csv", "markets_total": len(rows),
           "fade_longshots_h24": {"all_markets": fade(rows), "excluding_neg_risk": fade(single),
                                  "below_10c_in_multi_outcome_events": sum(1 for r in low if r["neg_risk"] == "True")},
           "calibration": {"h24": calibration(rows, "yes_price_24h"), "d7": calibration(rows, "yes_price_7d"),
                           "paired_markets_with_both_prices": {"h24": calibration(both, "yes_price_24h"), "d7": calibration(both, "yes_price_7d"),
                                                               "brier_h24": brier(both, "yes_price_24h"), "brier_d7": brier(both, "yes_price_7d")}},
           "kelly_table": kelly_table(), "kelly_fractions": kelly_drawdown(),
           "fade_at_95c": {"stake": 10000, "max_profit": round(10000 / 0.95 - 10000, 2), "wins_per_loss": round(0.95 / 0.05, 1)},
           "arbitrage_taker_fees": arbitrage_fee_examples()}
    with open(os.path.join(HERE, "pmm_article_figures.json"), "w", encoding="utf-8") as f:
        json.dump(res, f, indent=2)
    print(json.dumps(res, indent=2))


if __name__ == "__main__":
    main()
