"""Backtest the GROSS-MARGIN GATE: skip a book entirely unless the market's own
asks already sum to enough to survive a completion.

WHY. Live completions on 2026-07-30/31 showed the mechanism directly: every
completion that collected under ~101c of the payout LOST money once the ~1.3-
1.7c taker fee came off; every one that cleared it WON. "Asks sum above 100c" is
necessary but not sufficient -- a book only pays off if it completes near its
QUOTED price, and most of the time one leg has to be crossed into the BID
instead, giving up the spread on top of the fee. So the gate is applied at
POST TIME, using only information we already have before pricing a single leg:
gross = sum(market asks) - 100c. Skip the whole book below the threshold.

THIS RUNS THE ACTUAL LIVE POLICY (decaying arm + concede at T-60 + complete at
2/3 + flatten at 1/3), scored with the QUEUE-AWARE fill model
(research/queue_aware_fill.py, research/decay_backtest.py): a fill requires
cumulative volume at or above our price, SINCE WE LAST (RE)POSTED, to exceed the
depth resting there when we posted. Reprices reset queue position, matching the
live cancel-replace mechanics. This is a deliberate LOWER bound (assumes nobody
ahead of us ever cancels); see decay_backtest.py for why the optimistic model
(counts any print at our price as a fill) was rejected -- it disagreed with
queue-aware not just in magnitude but in DIRECTION on 2026-07-29's data.

Sweeps GROSS_GATE_C over several days and reports, per threshold: how many books
would have been posted, how many would have completed/flattened/gone empty, and
total P&L -- so the live constant (currently 2.5c) can be checked against
whatever the data actually supports rather than left at a guess.

Usage:  python research\\gross_gate_backtest.py [day ...]
        (defaults to every day with queue-depth logging: 07-28, 07-29, 07-30)
"""
import csv
import sys
from collections import defaultdict

sys.path.insert(0, r'E:\soccer\main')
sys.path.insert(0, r'E:\soccer\research')
import mm_settle as S
from decay_backtest import simulate
from queue_aware_fill import trades_sized

QUOTES = r'E:\soccer\research\mm_paper_quotes.csv'
DEFAULT_DAYS = ['2026-07-28', '2026-07-29', '2026-07-30']
THRESHOLDS = [0.0, 1.0, 1.5, 2.0, 2.5, 3.0, 4.0]


def load_day(day):
    rows = defaultdict(list)
    with open(QUOTES, encoding='utf-8') as fh:
        for r in csv.DictReader(fh):
            if (r['ts'][:10] == day and r.get('our_ask') and r.get('k_ask')
                    and r.get('pin_fair')):
                rows[r['leg_ticker']].append(r)
    by = defaultdict(dict)
    for tk, rs in rows.items():
        rs.sort(key=lambda x: x['ts'])
        by[rs[0]['event']][rs[0]['side']] = (tk, rs)
    by = {ev: legs for ev, legs in by.items() if len(legs) == 3}

    trades = {}
    for tk, rs in rows.items():
        ko = S._pt(rs[0]['kickoff'])
        trades[tk] = [(t, p, c) for t, p, c
                      in trades_sized(tk, S._pt(rs[0]['ts'])) if t < ko]
    return by, trades


MAX_SPREAD_C = 15.0    # mirrors mm_quoter.MAX_SPREAD_C


def gross_at_post(legs):
    """sum(market ask at FIRST snapshot) - 100c. What the gate sees at post time."""
    return sum(float(rs[0]['k_ask']) for _, rs in legs.values()) * 100 - 100


def coherent_at_post(legs):
    """Mirrors the live LIQUIDITY CHECK: reject only absurdly wide books, the
    signature of one nobody has quoted. "Botafogo" summed to 10c of bids against
    243c of asks -- a fake +143c overround from pure illiquidity.

    Do NOT also require sum(bids) < 100c. That was the first version and it was
    backwards: sum(bids) >= 100c means all three legs could be sold into the
    bids for more than the payout -- an arbitrage, and empirically our best
    books. Including it removed every book that cleared a 2.5c gate."""
    bid_sum = sum(float(rs[0]['k_bid']) for _, rs in legs.values()) * 100
    ask_sum = sum(float(rs[0]['k_ask']) for _, rs in legs.values()) * 100
    return (ask_sum - bid_sum) <= MAX_SPREAD_C


def main():
    days = sys.argv[1:] or DEFAULT_DAYS
    by_day = {}
    for day in days:
        by, trades = load_day(day)
        by_day[day] = (by, trades)
        print(f'{day}: {len(by)} three-leg books loaded')
    print()

    print(f"  {'gate':>5s} {'books':>6s} {'nat':>4s} {'cmpl':>5s} {'flat':>5s} "
          f"{'none':>5s}   {'P&L':>8s}   {'$/book':>7s}")
    for gate in THRESHOLDS:
        tot_n = tot_nat = tot_c = tot_f = tot_z = 0
        tot_pnl = 0.0
        for day, (by, trades) in by_day.items():
            gated = {ev: legs for ev, legs in by.items()
                     if coherent_at_post(legs) and gross_at_post(legs) >= gate}
            tot_n += len(gated)
            nat, comp, flat, none, pnl = simulate(gated, trades, 'decay_concede')
            tot_nat += nat; tot_c += comp; tot_f += flat; tot_z += none
            tot_pnl += pnl
        per_book = tot_pnl / tot_n if tot_n else float('nan')
        print(f'  {gate:5.1f} {tot_n:6d} {tot_nat:4d} {tot_c:5d} {tot_f:5d} '
              f'{tot_z:5d}   {tot_pnl:+8.2f}$ {per_book:+7.3f}$')

    print(f'\n  ({sum(len(b) for b, _ in by_day.values())} total books available '
          f'across {len(days)} days before gating)')
    print('  Live gate is set to 2.5c. Compare its row above against the sweep.')


if __name__ == '__main__':
    main()
