#!/usr/bin/env python3
"""v2 持续不变量检查（研究侧 RULING §9 ④）:
  任何时刻对 v2 baseline 跑交易集生成器, 必须**逐键复现 v2 baseline 键集合(0 差)**;
  出现"不可复现行"即报告并隔离, 不得静默留在 v2。

用法:
  python chk_v2_invariant.py                 # 生成当前键集合并与 baseline 比对, 打印台账
  python chk_v2_invariant.py --json out.json # 附机器可读结果
退出码: 0 = 0 差; 2 = 存在不可复现行(需隔离报告)
"""
import argparse, io, json, subprocess, sys
import pandas as pd

APP = '/Users/xpresso/zt_app'
sys.path.insert(0, f'{APP}/code')
from scan_daily_tb import COMBO, SELL_COMBO            # noqa: E402
from plot_ideal_top10 import chain_for_buy             # noqa: E402
import grid_or as g                                    # noqa: E402
import ideal_engine as ie                              # noqa: E402


def gen_keys():
    rows = []
    for pool in COMBO:
        bp = COMBO[pool]; sc = SELL_COMBO[pool]
        members, dailies = g.load_pool(pool)
        sig = g.build_sig(dailies, bp)
        trades = ie.ideal_backtest(pool, members, dailies, sig, sc['profit'], sc['dd'],
                                   mode=sc['mode'], stop=sc['stop'], hold=sc['hold'], start='20240201')
        for t in trades:
            d = dailies[t['code']].sort_values('trade_date').reset_index(drop=True)
            try:
                t0i, tai, tbi = chain_for_buy(d, t['code'], bp, str(t['buy_date']))
            except Exception:
                continue
            if t0i is None or tbi is None:
                continue
            rows.append({'code': t['code'], 'pool': pool, 'buy_date': str(t['buy_date']),
                         'tb': str(d['trade_date'].iloc[tbi])})
        print(f'{pool}: 累计 {len(rows)} 键', flush=True)
    return pd.DataFrame(rows)


def main():
    ap = argparse.ArgumentParser(); ap.add_argument('--json', default=None); a = ap.parse_args()
    base = pd.read_csv(f'{APP}/backtest_zt_full/strength_baseline.csv', dtype={'buy_date': str})
    cur = pd.read_csv(f'{APP}/patches/v2_pool_feat_20260919/V2_excluded_keys.csv', dtype={'buy_date': str})
    kbase = set(zip(base['code'], base['buy_date']))
    kexcl = set(zip(cur['code'], cur['buy_date']))
    now = gen_keys()
    kgen = set(zip(now['code'], now['buy_date']))
    missing = sorted(kbase - kgen - kexcl)      # baseline 有、重跑没有、且不在允许排除清单 -> 违规
    extra = sorted(kgen - kbase)                # baseline 没有、重跑有 -> 需报告(新增信号属正常, 但须留痕)
    unexcluded_offenders = sorted(kbase - kgen) # 不可复现行(应 ⊆ V2_excluded_keys)
    rogue = [k for k in unexcluded_offenders if k not in kexcl]
    res = {'baseline_keys': len(kbase), 'regen_keys': len(kgen),
           'not_reproduced_in_baseline': len(unexcluded_offenders),
           'excluded_allowed': len(kexcl), 'ROGUE_not_reproduced_and_not_excluded': len(rogue),
           'regen_new_keys': len(extra), 'rogue_sample': rogue[:20], 'new_sample': extra[:20],
           'verdict': 'PASS' if not rogue else 'FAIL_ISOLATE'}
    print('\n== v2 持续不变量 ==')
    for k, v in res.items():
        print(f'  {k}: {v}')
    if a.json:
        json.dump(res, open(a.json, 'w'), ensure_ascii=False, indent=1)
    if rogue:
        print('\n!! 存在不可复现且未列入排除清单的键, 需隔离报告(不得静默留在 v2 baseline):', rogue[:20])
        sys.exit(2)
    sys.exit(0)


if __name__ == '__main__':
    main()
