"""Do legs on wildly-swinging games fill more often?

The hypothesis is a good one and the fill model cannot answer it: of its 24
features not one measures price movement. It has spread, overround, volume,
depth, time -- all snapshots of a moment, nothing about how much the price
travels.

The mechanism would be simple. A resting ask fills when a buyer crosses it. A
leg whose fair value wanders re-tests our price repeatedly; a leg pinned at one
number never does. So movement should buy fills.

CIRCULARITY IS THE WHOLE DIFFICULTY. Measuring a leg's volatility over its full
quoting life and correlating that with whether it filled is close to measuring
the fill itself: the move that lifted us is inside the window. So volatility is
measured over an EARLY window only -- the leg's first `EARLY_N` snapshots -- and
the fill outcome is taken from the whole life. Legs whose price history is
shorter than the early window are dropped rather than scored on a partial one.

That still leaves a leg that filled inside the early window contaminating its
own predictor, which is why the split is reported both ways: all legs, and legs
that survived past the early window.
"""
import csv, collections, statistics, sys

QUOTES = r'E:\soccer\research\mm_paper_quotes.csv'
RESULTS = r'E:\soccer\research\mm_paper_results.csv'
import os
# Snapshots (~20s apart) used to measure movement. 15 was the first try and
# was useless: over ~5 minutes 84% of legs never moved a cent, so every
# quartile boundary landed on 0.00c and the buckets collapsed into one.
# Kalshi soccer prices sit still on a 1c grid for long stretches; to see a
# game 'swing' the window has to be measured in hours.
EARLY_N = int(os.environ.get('EARLY_N', 180))   # ~1 hour


def _f(x):
    try:
        v = float(x)
        return v if 0 < v < 1 else None
    except (TypeError, ValueError):
        return None


def load_prices():
    """{leg_ticker: [k_ask in time order]}"""
    seq = collections.defaultdict(list)
    rows = collections.defaultdict(list)
    with open(QUOTES, encoding='utf-8', errors='replace') as fh:
        for r in csv.DictReader(fh):
            t, a, ts = r.get('leg_ticker'), _f(r.get('k_ask')), r.get('ts')
            if t and a and ts:
                rows[t].append((ts, a))
    for t, rs in rows.items():
        rs.sort()
        seq[t] = [a for _, a in rs]
    return seq


def load_fills():
    """{leg_ticker: filled 0/1}"""
    out = {}
    with open(RESULTS, encoding='utf-8', errors='replace') as fh:
        for r in csv.DictReader(fh):
            t = r.get('leg_ticker')
            if not t:
                continue
            f = str(r.get('filled') or '0').strip() in ('1', 'True', 'true')
            out[t] = out.get(t, 0) or int(f)
    return out


def vol_of(prices):
    """Movement in the early window: stdev, range and number of changes."""
    early = prices[:EARLY_N]
    if len(early) < EARLY_N:
        return None
    sd = statistics.pstdev(early)
    rng = max(early) - min(early)
    changes = sum(1 for a, b in zip(early, early[1:]) if abs(b - a) > 1e-9)
    return {'sd': sd, 'range': rng, 'changes': changes}


def report(rows, metric, label, buckets):
    print(f'\n  by {label}:')
    print(f'    {"bucket":>14} {"legs":>6} {"filled":>7} {"fill rate":>10}')
    got = []
    for lo, hi, name in buckets:
        sel = [r for r in rows if lo <= r[metric] < hi]
        if len(sel) < 15:
            continue
        f = sum(r['filled'] for r in sel)
        got.append((name, len(sel), 100.0 * f / len(sel)))
        print(f'    {name:>14} {len(sel):>6} {f:>7} {100.0*f/len(sel):>9.1f}%')
    if len(got) >= 2:
        lo_r, hi_r = got[0][2], got[-1][2]
        print(f'    -> {got[-1][0]} vs {got[0][0]}: {hi_r-lo_r:+.1f} '
              f'percentage points')


def main():
    seq, fills = load_prices(), load_fills()
    rows = []
    for t, prices in seq.items():
        if t not in fills:
            continue
        v = vol_of(prices)
        if not v:
            continue
        v['filled'] = fills[t]
        v['life'] = len(prices)
        rows.append(v)

    if not rows:
        print('no legs with enough price history')
        return
    base = 100.0 * sum(r['filled'] for r in rows) / len(rows)
    print(f'{len(rows)} legs with >= {EARLY_N} snapshots of history, '
          f'base fill rate {base:.1f}%')

    sds = sorted(r['sd'] for r in rows)
    q = [sds[int(p * len(sds))] for p in (0.25, 0.5, 0.75)]
    print(f'\nearly-window stdev quartiles: '
          f'{q[0]*100:.2f}c / {q[1]*100:.2f}c / {q[2]*100:.2f}c')

    report(rows, 'sd', 'price stdev over the first %d snapshots' % EARLY_N,
           [(0, q[0], 'calmest 25%'), (q[0], q[1], '2nd'),
            (q[1], q[2], '3rd'), (q[2], 9, 'wildest 25%')])
    report(rows, 'changes', 'number of price changes',
           [(0, 1, 'never moved'), (1, 3, '1-2'), (3, 6, '3-5'), (6, 99, '6+')])

    # the contamination check: legs that lived well past the early window
    survivors = [r for r in rows if r['life'] > EARLY_N * 2]
    if len(survivors) > 60:
        print(f'\n=== legs that survived past the early window '
              f'({len(survivors)}, so the measured move came BEFORE any fill) ===')
        s = sorted(r['sd'] for r in survivors)
        qq = [s[int(p * len(s))] for p in (0.25, 0.5, 0.75)]
        report(survivors, 'sd', 'price stdev (uncontaminated)',
               [(0, qq[0], 'calmest 25%'), (qq[0], qq[1], '2nd'),
                (qq[1], qq[2], '3rd'), (qq[2], 9, 'wildest 25%')])


if __name__ == '__main__':
    main()
