"""One-shot reminder: review xhigh, and re-check a Poly-US-only rule.

Scheduled for 2026-09-13 (about five weeks after the 2026-08-09 freeze). Emails
a live summary read straight out of the forward-test log, then the Windows task
that fires it is a one-off, so nothing recurs.

WHY A SCHEDULED TASK AND NOT A CHAT REMINDER: Claude's own cron jobs are
session-only and expire after 7 days, so they cannot carry five weeks. This runs
off Task Scheduler on the box, which survives reboots and closed sessions.

WHAT TO CHECK, AND THE TRAPS
  * xhigh was frozen 2026-08-09 (Kalshi-only, mid>=0.20, CLV in [0.05,0.12)).
    It needs ~480 bets. Anything below that is directional only.
  * Its discovery numbers (+15.1%, z+1.33) came from post-2026-07-05 data that
    was ALREADY the xlow forward-test window -- selection, not validation. Only
    fixtures after the freeze count. Expect the effect to shrink.
  * Poly-US-only was swept on 2026-08-09 and NOT frozen: the price-floor effect
    reproduced (+16.1% at minP>=0.20) but train and test disagreed in sign, and
    persistence hurt. Deliberately deferred to avoid a second frozen rule
    doubling the multiple-comparison exposure.
  * xhigh and xlow OVERLAP -- both fire on Kalshi CLV in the same band. If both
    look good, that is largely the same bets, not two pieces of evidence.
  * Better than freezing a Poly-US rule: split xhigh's OWN realised bets by
    venue. That answers "is the edge venue-dependent" for free.
"""
import collections
import csv
import json
import math
import statistics
import sys
from pathlib import Path

sys.path.insert(0, r'E:\soccer\main')
from daily_signals import send_email

LOG = Path(r'E:\soccer\research\forward_test_unique_book_log.csv')
CAPTURE = Path(r'E:\soccer\research\pin_fresh_capture.csv')
ACC_CACHE = Path(r'E:\soccer\research\pin_accuracy_cache.json')


def latest():
    try:
        rows = [r for r in csv.DictReader(open(LOG, encoding='utf-8'))]
        return rows[-1] if rows else {}
    except Exception:
        return {}


def _f(x):
    try:
        return float(x)
    except (TypeError, ValueError):
        return None


def pinnapi_summary():
    """Live read of the pinnapi-vs-API-Football comparison.

    Three separate questions, deliberately reported apart because they answered
    differently as of 2026-08-09 and conflating them caused real confusion:

      ACCURACY  paired Brier per fixture. The only clean test -- paired, uses
                every fixture, no entry reconstruction. Needed ~580 fixtures.
                It FLIPPED SIGN (+0.0096 -> -0.0078) when n went 26 -> 77, which
                is why nothing below target should be believed.
      DIVERGENCE how often the two feeds produce a different bet. This is NOT
                evidence either is better -- only that the choice matters.
      BIAS      API-Football rounds odds to 2dp (1.50 where pinnapi says
                1.5917). Measured as a monotonic gradient: it underprices
                favourites and overprices long shots. This is a MECHANISM and
                was stable across sample sizes.
    """
    out = {}
    if not CAPTURE.exists():
        return out
    rows = list(csv.DictReader(open(CAPTURE, encoding='utf-8')))
    out['snapshots'] = len(rows)

    # signal divergence, per FIXTURE (snapshot-level agreement is ~98% and
    # meaningless -- it is dominated by fixtures where neither feed fires)
    fx = collections.defaultdict(lambda: {'s': set(), 'f': set()})
    for r in rows:
        k = (r['lc'], r['home'], r['away'], r['game_date'])
        if r.get('xlow_stale'):
            fx[k]['s'].add(r['xlow_stale'])
        if r.get('xlow_fresh'):
            fx[k]['f'].add(r['xlow_fresh'])
    sig = {k: v for k, v in fx.items() if v['s'] or v['f']}
    n = len(sig) or 1
    out['sig_fixtures'] = len(sig)
    out['same'] = sum(1 for v in sig.values() if v['s'] and v['s'] == v['f'])
    out['af_only'] = sum(1 for v in sig.values() if v['s'] and not v['f'])
    out['pin_only'] = sum(1 for v in sig.values() if v['f'] and not v['s'])
    out['differ'] = sum(1 for v in sig.values()
                        if v['s'] and v['f'] and v['s'] != v['f'])
    out['agree_pct'] = 100.0 * out['same'] / n

    # rounding bias by price bucket, signed (API-Football minus pinnapi)
    sg = collections.defaultdict(list)
    for r in rows:
        for t in ('h', 'a'):
            fr, st = _f(r.get(f'fair_fresh_{t}')), _f(r.get(f'fair_stale_{t}'))
            if fr is None or st is None:
                continue
            b = 'fav' if fr >= 0.5 else ('mid' if fr >= 0.25 else 'dog')
            sg[b].append(st - fr)
    for b in ('fav', 'mid', 'dog'):
        if sg[b]:
            out[f'bias_{b}'] = 100.0 * statistics.mean(sg[b])

    # paired accuracy, recomputed from the cached settled results
    try:
        cache = json.loads(ACC_CACHE.read_text(encoding='utf-8'))
    except Exception:
        cache = {}
    last = {}
    for r in rows:
        k = f"{r['lc']}|{r['home']}|{r['away']}|{r['game_date']}"
        if k not in last or r['ts'] > last[k]['ts']:
            last[k] = r
    diffs = []
    for k, r in last.items():
        ftr = cache.get(k)
        if not ftr:
            continue
        fh, fa = _f(r['fair_fresh_h']), _f(r['fair_fresh_a'])
        sh, sa = _f(r['fair_stale_h']), _f(r['fair_stale_a'])
        if None in (fh, fa, sh, sa):
            continue
        yh, ya = 1.0 * (ftr == 'H'), 1.0 * (ftr == 'A')
        diffs.append(((sh - yh) ** 2 + (sa - ya) ** 2)
                     - ((fh - yh) ** 2 + (fa - ya) ** 2))
    if len(diffs) >= 10:
        m = statistics.mean(diffs)
        sd = statistics.pstdev(diffs) or 1e-9
        se = sd / math.sqrt(len(diffs))
        out.update(acc_n=len(diffs), acc_mean=m, acc_se=se, acc_z=m / se,
                   acc_lo=m - 1.96 * se, acc_hi=m + 1.96 * se)
    return out


def build_pin_html(p):
    if not p:
        return '<h3>pinnapi vs API-Football</h3><p>No capture data found.</p>'
    if 'acc_n' in p:
        verdict = ('<b>pinnapi more accurate</b>' if p['acc_lo'] > 0 else
                   ('<b>API-Football more accurate</b>' if p['acc_hi'] < 0 else
                    '<b>no measurable difference</b>'))
        target = '' if p['acc_n'] >= 580 else (
            f" &mdash; still {580 - p['acc_n']} short of the 580 target, "
            "so treat as directional")
        acc = (f"<li><b>Accuracy (paired Brier, n={p['acc_n']}):</b> "
               f"{p['acc_mean']:+.5f} (SE {p['acc_se']:.5f}, z {p['acc_z']:+.2f}), "
               f"95% CI [{p['acc_lo']:+.5f}, {p['acc_hi']:+.5f}] &rarr; "
               f"{verdict}{target}</li>")
    else:
        acc = '<li><b>Accuracy:</b> too few settled fixtures to report.</li>'
    bias = ' / '.join(f"{b} {p.get(f'bias_{b}', float('nan')):+.2f}c"
                      for b in ('fav', 'mid', 'dog') if f'bias_{b}' in p)
    return f"""
<h3>pinnapi vs API-Football</h3>
<ul>
  {acc}
  <li><b>Signal divergence ({p.get('sig_fixtures', 0)} fixtures that produced a
      signal):</b> identical {p.get('same', 0)}
      ({p.get('agree_pct', 0):.0f}%), API-Football only {p.get('af_only', 0)},
      pinnapi only {p.get('pin_only', 0)}, both fired but DIFFERENT bet
      {p.get('differ', 0)}. Divergence shows the feed choice <i>matters</i>;
      it is not evidence either is better.</li>
  <li><b>Rounding bias (API-Football minus pinnapi):</b> {bias}. A monotonic
      gradient = API-Football's 2dp rounding underpricing favourites and
      overpricing long shots. This was stable across sample sizes &mdash; the
      one solid finding of the exercise.</li>
</ul>
<p><b>Traps.</b> The accuracy figure <b>flipped sign</b> (+0.0096 &rarr;
&minus;0.0078) when n went 26&rarr;77, so anything below target is noise. And a
positive result justifies <b>recalibrating the xlow band against the fresh
line and re-testing</b> &mdash; NOT switching the live feed, which stays on
API-Football so the frozen forward tests keep running. Given the rounding
gradient, a band tuned to a biased anchor will not transfer to an unbiased one.</p>
<p>Re-run: <code>python research\\pin_accuracy_test.py</code> and
<code>python research\\pin_fresh_vs_stale.py</code></p>
"""


def main():
    r = latest()
    n = r.get('xhigh_n') or '0'
    pin_html = build_pin_html(pinnapi_summary())
    body = f"""
<h2>Reminder: review xhigh (and revisit Poly-US)</h2>

<p>Frozen 2026-08-09. Set as a five-week checkpoint.</p>

<h3>Where xhigh stands (from the forward-test log, run {r.get('run_date','?')})</h3>
<ul>
  <li><b>n = {n}</b> of the ~480 needed to resolve the effect</li>
  <li>WR {r.get('xhigh_wr','-')}% &nbsp; ROI {r.get('xhigh_roi','-')}% &nbsp;
      z {r.get('xhigh_z','-')}</li>
  <li>xlow for comparison: n={r.get('xlow_n','-')} ROI {r.get('xlow_roi','-')}%
      z {r.get('xlow_z','-')}</li>
</ul>

<h3>Read it with these in mind</h3>
<ul>
  <li>xhigh's discovery numbers (+15.1%) came from data that was already the
      xlow forward-test window. That was <b>selection, not validation</b> &mdash;
      expect the real effect to be smaller.</li>
  <li>The reason it was frozen was <b>measurability</b>, not ROI: a price floor
      cut the required sample from ~201,000 bets to ~480. 17 candidates were
      swept and none reached z&gt;1.96.</li>
  <li><b>xhigh overlaps xlow.</b> Both fire on Kalshi CLV in the same band. Two
      green numbers here are largely the same bets.</li>
  <li>If n is still well short of 480, say so and wait. Do not read a partial
      sample as a verdict.</li>
</ul>

<h3>The Poly-US question that was deferred</h3>
<p>Swept 2026-08-09, not frozen: the price floor reproduced (+16.1% at
minP&ge;0.20, needing only ~370 bets) but <b>train and test disagreed in sign</b>
and persistence hurt every variant. The cheaper next step is to split xhigh's own
realised bets by venue rather than freeze a second rule.</p>

<p>Poly-US also pays a <b>maker rebate</b> where Kalshi charges a taker fee. If
the floor effect survives, that is an execution reason to prefer it &mdash; a
separate question from whether the signal is better.</p>

<p>Re-run: <code>python research\\rule_sweep.py</code> and
<code>python research\\forward_test_unique_book.py</code></p>

{pin_html}
"""
    ok = send_email('[REMINDER] Review xhigh + revisit Poly-US rule', body)
    print('sent' if ok else 'send failed')
    return 0 if ok else 1


if __name__ == '__main__':
    sys.exit(main())
