"""Can a multi-book consensus stand in for Pinnacle as the MAKER's fair value?

WHY THIS MATTERS. mm_quoter skips any fixture without a Pinnacle line. On
2026-07-29 that cost 4 of 10 in-window fixtures -- all four had healthy 3-leg
Kalshi books and were dropped purely because API-Football's relay carries no
Pinnacle for them. Coverage is 92-100% per league, so these are a genuine tail,
not a timing lag (Internacional and Mirassol, same league and day, do have it).

THE ARGUMENT FOR A FALLBACK. The betting strategy needs Pinnacle specifically --
its entire edge is defined as a deviation FROM Pinnacle, so substituting a worse
book would change what is being measured. The market maker does not: it needs a
LEVEL to quote around, and its ask is capped at the market's own ask anyway. A
consensus of 8-11 other bookmakers is plausibly good enough for that.

THE TEST. On fixtures where Pinnacle IS present, build the consensus WITHOUT it
and measure how far the consensus fair lands from Pinnacle's fair, in cents of
implied probability. That is the error we would be introducing on the fixtures
where we currently quote nothing at all.

Judge it against the quantities that matter, not against zero:
  - the 1c tick: an error below ~0.5c cannot move our quoted price at all
  - the typical margin we add: 2-6c
  - FAIR_TOLERANCE_C = 2.0, the point at which mm_quoter refuses to sell

Usage:  python research\\consensus_fair_test.py [n_fixtures]
"""
import statistics
import sys
from collections import defaultdict

import requests

sys.path.insert(0, r'E:\soccer\main')
import live_monitor as LM

EXCLUDE = {'pinnacle'}


def devig(o):
    r = [1.0 / x for x in o]
    s = sum(r)
    return [x / s for x in r]


def books_for(fid):
    """{book_name: [h,d,a] decimal odds} for the match-winner market."""
    try:
        r = requests.get(f"{LM.AF_BASE}/odds", headers=LM.AF_HEADERS,
                         params={"fixture": fid, "bet": 1}, timeout=15)
        out = {}
        for e in r.json().get('response', []):
            for bk in e.get('bookmakers', []):
                for bet in bk.get('bets', []):
                    if bet.get('id') != 1:
                        continue
                    v = {x['value'].lower(): float(x['odd']) for x in bet['values']}
                    if all(k in v for k in ('home', 'draw', 'away')):
                        out[bk['name']] = [v['home'], v['draw'], v['away']]
        return out
    except Exception:
        return {}


def main():
    want = int(sys.argv[1]) if len(sys.argv) > 1 else 40
    _, ms = LM.load_watchlist_file()
    rows, skipped = [], 0
    for m in ms:
        if len(rows) >= want:
            break
        fid = m.get('fid')
        if not fid:
            continue
        bk = books_for(fid)
        pin = next((v for k, v in bk.items() if 'pinnacle' in k.lower()), None)
        others = {k: v for k, v in bk.items() if k.lower() not in EXCLUDE
                  and 'pinnacle' not in k.lower()}
        if not pin or len(others) < 3:
            skipped += 1
            continue
        pf = devig(pin)
        # consensus = per-outcome MEDIAN of each book's devigged probability.
        # Median not mean: one stale or badly-priced book should not drag the
        # level, and with 8-11 books the median is well determined.
        devigged = [devig(v) for v in others.values()]
        cons = [statistics.median([d[i] for d in devigged]) for i in range(3)]
        s = sum(cons)
        cons = [c / s for c in cons]           # renormalise after the median
        rows.append({'div': m.get('lc'), 'home': m['home'], 'n_books': len(others),
                     'diff_c': [abs(a - b) * 100 for a, b in zip(pf, cons)],
                     'pf': pf, 'cons': cons})

    if not rows:
        print('no fixtures with both Pinnacle and >=3 other books')
        return
    print(f'{len(rows)} fixtures with Pinnacle + >=3 other books ({skipped} skipped)\n')
    print(f"  {'lg':5s} {'fixture':18s} {'books':>5s} {'home':>7s} {'draw':>7s} {'away':>7s}")
    for r in rows[:18]:
        d = r['diff_c']
        print(f"  {str(r['div']):5s} {r['home'][:18]:18s} {r['n_books']:5d} "
              f"{d[0]:6.2f}c {d[1]:6.2f}c {d[2]:6.2f}c")

    alld = [x for r in rows for x in r['diff_c']]
    alld.sort()
    print(f'\n  |consensus - Pinnacle| across {len(alld)} legs:')
    print(f'    median {statistics.median(alld):.2f}c   '
          f'p75 {alld[int(len(alld)*.75)]:.2f}c   '
          f'p90 {alld[int(len(alld)*.90)]:.2f}c   max {max(alld):.2f}c')
    print(f'    within 0.5c (cannot move our 1c-tick quote): '
          f'{sum(1 for x in alld if x<0.5)/len(alld)*100:.0f}%')
    print(f'    within 1.0c: {sum(1 for x in alld if x<1.0)/len(alld)*100:.0f}%')
    print(f'    beyond 2.0c (would breach FAIR_TOLERANCE_C): '
          f'{sum(1 for x in alld if x>2.0)/len(alld)*100:.0f}%')
    print('\n  Reference: our staged margin is 2-6c, and the tick is 1c.')


if __name__ == '__main__':
    main()
