#!/usr/bin/env python3
"""冻结 pool-feat-v1 档案（方案甲）——按研究侧 ② 的三项要求：
  1) 持久化 107 行存疑行清单（键 + 原因"修准后 tb 不可复现"）
  2) 档案 README 写明性质与影响面
  3) v1 档案只读冻结 + sha256 清单

全程只读现行库（baseline/model/nav/state 仅拷贝），不写任何权威文件。

用法: python freeze_v1_archive.py [--out patches/archive_pool_feat_v1_20260914]
"""
import argparse, hashlib, io, os, shutil, subprocess, sys
import pandas as pd

APP = '/Users/xpresso/zt_app'
BASE = f'{APP}/backtest_zt_full'
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


def sha16(p):
    h = hashlib.sha256(open(p, 'rb').read()).hexdigest()
    return h, h[:16]


def check_rows(base):
    """逐行核 tb 可复现性：chain 推不出 tb ⇒ 存疑；chain tb ≠ 日历 tb ⇒ 不一致。"""
    need = sorted(set(base['pool']))
    loaded = {p: g.load_pool(p) for p in COMBO if p in need}
    susp, mism = [], []
    for _, r in base.iterrows():
        pool, code, bd = r['pool'], r['code'], str(r['buy_date'])
        if pool not in loaded:
            continue
        members, dailies = loaded[pool]
        if code not in dailies:
            susp.append((code, bd, pool, 'code 不在该池日线目录'))
            continue
        d = dailies[code].sort_values('trade_date').reset_index(drop=True)
        bp = COMBO[pool]
        try:
            t0i, tai, tbi = chain_for_buy(d, code, bp, bd)
        except Exception as e:
            susp.append((code, bd, pool, f'chain 异常 {type(e).__name__}'))
            continue
        if t0i is None or tbi is None:
            susp.append((code, bd, pool, '修准后 tb 不可复现'))
            continue
        tb_chain = str(d['trade_date'].iloc[tbi])
        prior = d[d['trade_date'] < bd]['trade_date']
        if len(prior) == 0:
            susp.append((code, bd, pool, '无早于 buy_date 的交易日'))
            continue
        tb_cal = str(prior.iloc[-1])
        if tb_chain != tb_cal:
            mism.append((code, bd, pool, tb_chain, tb_cal))
    S = pd.DataFrame(susp, columns=['code', 'buy_date', 'pool', '原因'])
    M = pd.DataFrame(mism, columns=['code', 'buy_date', 'pool', 'chain_tb', '日历tb'])
    return S, M


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--out', default=f'{APP}/patches/archive_pool_feat_v1_20260914')
    a = ap.parse_args()
    out = a.out
    os.makedirs(out, exist_ok=True)
    files = ['strength_baseline.csv', 'strength_model.json', 'sim_trading_nav.csv', 'sim_trading_state.json']
    for f in files:
        src = f'{BASE}/{f}' if os.path.exists(f'{BASE}/{f}') else f'{APP}/{f}'
        shutil.copy2(src, f'{out}/{f}')
    for f in ['pool_signal_count.csv', 'pool_daily_stats.csv', 'report_backfill_v1.md',
              'report_model_compare_v1.md', 'diff_170rows_v1.csv', 'stat_variants.csv']:
        src = f'{APP}/patches/pool_caches_v1_20260914/{f}'
        if os.path.exists(src):
            shutil.copy2(src, f'{out}/v1_{f}')
    base = pd.read_csv(f'{out}/strength_baseline.csv', dtype={'buy_date': str})
    print(f'档案 baseline 行数 {len(base)}', flush=True)
    S, M = check_rows(base)
    S['原因'] = S['原因'].fillna('修准后 tb 不可复现')
    S.to_csv(f'{out}/V1_107存疑行清单.csv', index=False)
    M.to_csv(f'{out}/V1_chain与日历tb不一致.csv', index=False)
    print(f'存疑行 {len(S)} 行；chain≠日历tb {len(M)} 行', flush=True)
    L = []
    L.append('# pool-feat-v1 档案（只读冻结，方案甲）\n')
    L.append('## 性质')
    L.append('- 本目录是 **pool-feat-v1 期实盘/回测记录的如实存档**：baseline（记录版世代，含 0914 当日增量）+')
    L.append('  当时模型 + 模拟盘 nav/state + v1 池级两表与对照报告。')
    L.append('- **只读冻结**：任何后续口径（v2）不得回写本目录。已 `chmod 444` 并附 `SHA256SUMS`。')
    L.append('- 生效区间：v1 = 起点至 2026-09-18（落库前最后一日）；v2 自 2026-09-21（周一）首个交易日生效，只向前。')
    L.append('  0911~0914 的 golden 记录不重算：0911 净值锚点 **90849.65** 降级为"v1 期锚点"，不作跨版本判据。\n')
    L.append('## 影响面（必须随档案一起读）')
    L.append(f'- 本 baseline 行数 **{len(base)}**，其中 **{len(S)} 行**为"修准前写入、修准后 tb 不可复现"的存疑行：')
    L.append('  存疑行**原值保留、不改写**（改写会造出"档案里混入 v2 口径"的伪记录），逐键清单见 `V1_107存疑行清单.csv`。')
    L.append(f'- 另有 **{len(M)} 行** chain 推出的 tb 与日历口径 tb 不一致（见 `V1_chain与日历tb不一致.csv`），同属记录版世代特征。')
    L.append('- 约定：**v2 期任何统计/回测不得把上述存疑行并入样本**；v2 由修准后数据重生成，天然不含它们。')
    L.append('- 跨版本差异（v1→v2 的键集合增删与 strength 变化）**属预期**，按段对账、不逐值追责。\n')
    L.append('## 数据世代依赖（登记项）')
    L.append('- 本档案的键集合与数值属 **修准前（adj_factor 抖动世代）**。`_adjfactor_repair_backup/` 快照')
    L.append('  **只能用于归因**（如 12 键差隔离实验），**不得用于验收比对**。')
    L.append('- 验收比对一律使用修准后数据（现行 `backtest_zt_full/daily_*`）。\n')
    L.append('## 冻结清单（sha256[:16]）')
    rows = []
    for f in sorted(os.listdir(out)):
        if f in ('README.md', 'SHA256SUMS'):
            continue
        h, h16 = sha16(f'{out}/{f}')
        rows.append((f, h16, h))
        L.append(f'- `{f}` — {h16}')
    open(f'{out}/SHA256SUMS', 'w').write('\n'.join(f'{h}  {f}' for f, _, h in rows) + '\n')
    open(f'{out}/README.md', 'w').write('\n'.join(L) + '\n')
    for f in os.listdir(out):
        os.chmod(f'{out}/{f}', 0o444)
    print('已冻结:', out)


if __name__ == '__main__':
    main()
