#!/usr/bin/env python3
"""池级三特征回填 + 对照报告（RULING c33b091 §3/§4；沙盒）

tb 推导（双路交叉验证）:
  主路 tb_cal  = 池日历中**严格早于 buy_date 的最近交易日**（scan 约定: buy_date = tb 的下一交易日）
  校验 tb_chain = chain_for_buy(d, code, bp, buy_date)（与扫描同一实现）
  不一致 / 推不出 ⇒ 标 tb_uncertain（0913 数据修准后信号不再成立的那批）

池级特征（口径已按对端复核更正）:
  成员集 = 该池日线目录当日全部有行情文件（**不用** members.pkl 名单）
  f13_pool_zt     = n_zt(tb)/20
  f14_pool_avgret = avg_ret(tb)（百分数）
  f12_crowd_pool  = C(tb) / max(1, mean(C(tb-250td..tb-1)))   [C = pool_signal_count.n_sig, c_mean250 已预计算]
  NaN: 该 (pool,tb) 在 pool_daily_stats 中不存在（当日无任何行情文件）-> 三特征 NaN（不插值）

用法: python backfill_pool_features.py --cache-dir /tmp/pool_cache_v1 --baseline <csv> --out <csv> --report <md>
"""
import argparse, os, sys
import numpy as np
import pandas as pd

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import grid_or as g
from scan_daily_tb import COMBO
from plot_ideal_top10 import chain_for_buy

POOLS = ['hs300', 'zz500', 'zz1000', 'zz2000']
F3 = ['f12_crowd_pool', 'f13_pool_zt', 'f14_pool_avgret']
POOL_TOL = 1e-2   # 裁定 §5: 池级容差 = 绝对值 1e-2


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--cache-dir', default='/tmp/pool_cache_v1')
    ap.add_argument('--baseline', default=f'{g.BASE}/strength_baseline.csv')
    ap.add_argument('--out', default='/tmp/baseline_backfilled_v1.csv')
    ap.add_argument('--report', default='/tmp/pool_backfill_report_v1.md')
    ap.add_argument('--diffs', default='/tmp/pool_backfill_diffs_v1.csv')
    a = ap.parse_args()

    stats = pd.read_csv(f'{a.cache_dir}/pool_daily_stats.csv', dtype={'trade_date': str})
    sigs = pd.read_csv(f'{a.cache_dir}/pool_signal_count.csv', dtype={'trade_date': str})
    df = pd.read_csv(a.baseline, dtype={'buy_date': str, 'sell_date': str})
    n0 = len(df)

    cal = {p: sorted(set(stats[stats['pool'] == p]['trade_date'])) for p in POOLS}
    stat_map = {(r.pool, r.trade_date): (int(r.n_zt), float(r.avg_ret), int(r.n_codes)) for r in stats.itertuples()}
    sig_map = {(r.pool, r.trade_date): (int(r.n_sig), float(r.c_mean250)) for r in sigs.itertuples()}

    def tb_cal(pool, bd):
        prev = [d for d in cal[pool] if d < bd]
        return prev[-1] if prev else None

    rec = {}
    for pool in COMBO:
        bp = COMBO[pool]
        _, dailies = g.load_pool(pool)
        for code, sub in df[df['pool'] == pool].groupby('code'):
            d = dailies.get(code)
            d = None if d is None else d.sort_values('trade_date').reset_index(drop=True)
            for bd in sub['buy_date']:
                tc = tb_cal(pool, bd)
                tch = None
                if d is not None:
                    try:
                        t0i, tai, tbi = chain_for_buy(d, code, bp, str(bd))
                        tch = None if t0i is None else str(d['trade_date'].iloc[tbi])
                    except Exception:
                        tch = None
                rec[(pool, code, bd)] = (tc, tch)
        print(f'{pool}: tb 推导+校验完成', flush=True)

    out = []
    for r in df.itertuples():
        tc, tch = rec.get((r.pool, r.code, r.buy_date), (None, None))
        unc = (tch is None) or (tch != tc)
        f12 = f13 = f14 = np.nan
        day_missing = True
        if tc is not None and (r.pool, tc) in stat_map:
            nzt, avg, nc = stat_map[(r.pool, tc)]
            if nc > 0:
                day_missing = False
                f13, f14 = nzt / 20.0, avg
                c, cm = sig_map.get((r.pool, tc), (0, np.nan))
                den = max(1.0, cm) if not np.isnan(cm) else 1.0
                f12 = c / den
        out.append({'tb': tc, 'tb_chain': tch, 'tb_uncertain': bool(unc), 'day_missing': bool(day_missing),
                    'f12_new': f12, 'f13_new': f13, 'f14_new': f14})
    nw = pd.DataFrame(out, index=df.index)
    for c, src in zip(F3, ['f12_new', 'f13_new', 'f14_new']):
        df[c + '_old'] = df[c].astype(float)
        df[c] = nw[src].values
    df['tb'] = nw['tb'].values
    df['tb_chain'] = nw['tb_chain'].values
    df['tb_uncertain'] = nw['tb_uncertain'].values
    df['pool_feat_nan'] = nw[['f12_new', 'f13_new', 'f14_new']].isna().any(axis=1).values
    df['pool_feat_nan_reason'] = np.where(nw['day_missing'], 'day_no_quote', '')
    df.to_csv(a.out, index=False)

    diffs = df[df[[c + '_old' for c in F3]].notna().any(axis=1)][
        ['code', 'pool', 'buy_date', 'tb', 'tb_chain', 'tb_uncertain'] + sum([[c + '_old', c] for c in F3], [])]
    diffs.to_csv(a.diffs, index=False)

    L = ['# 池级特征回填 · 对照报告 v1（Mac 侧，沙盒；口径 = RULING c33b091）\n']
    L.append(f'- 输入 baseline: `{a.baseline}`（{n0} 行）；缓存: `{a.cache_dir}`（目录口径，非 members.pkl 名单）')
    L.append(f'- 输出基线副本: `{a.out}`；170 行逐行留痕: `{a.diffs}`（{len(diffs)} 行）\n')
    L.append('## ① 两张缓存表行数与首末日期\n')
    for nm_, t in [('pool_signal_count.csv（含 n_sig=C(d), c_mean250）', sigs), ('pool_daily_stats.csv（n_zt/avg_ret/n_codes）', stats)]:
        for p in POOLS:
            s = t[t['pool'] == p]
            L.append(f'- {nm_} · {p}: **{len(s)} 行**, {s["trade_date"].min()} ~ {s["trade_date"].max()}')
    L.append('')
    L.append('## ② tb 推导交叉验证（日历规则 vs chain_for_buy）\n')
    none_i = nw['tb_chain'].isna() & nw['tb_uncertain']
    mism_i = (~nw['tb_chain'].isna()) & nw['tb_uncertain']
    L.append(f'- 全表 {n0} 行: 两路一致 **{int((~nw["tb_uncertain"]).sum())}** 行, 存疑 **{int(nw["tb_uncertain"].sum())}** 行'
             f'（chain 推不出 {int(none_i.sum())} 行; chain ≠ 日历 tb {int(mism_i.sum())} 行）')
    L.append(f'- chain 推不出的 buy_date 分布: {df[none_i.values]["buy_date"].value_counts().sort_index().to_dict()}')
    L.append('- 定性: 集中在 20260826~20260910（0913 修准前写入的信号行）⇒ 现行数据口径下信号不成立；'
             '与"方案 B 掉键"同一批。处置待 Owner 签批（本报告保守填值不删行）。\n')
    L.append('## ③ 170 行: tb 口径重算 vs 原存值（原值 = 全目录+买入日 口径的历史实时值）\n')
    L.append(f'- 容差基准（裁定 §5）: 池级 **绝对 1e-2**；判据以 strength 1e-4 为准（strength 需全表重派生后另比）')
    for c in F3:
        o, w = diffs[c + '_old'].astype(float), diffs[c].astype(float)
        both = o.notna() & w.notna()
        ad = (w[both] - o[both]).abs()
        L.append(f'- **{c}**: 新旧皆非空 {int(both.sum())} 行; |Δ| 中位 {np.nanmedian(ad):.6f} / p90 {np.nanpercentile(ad.dropna(), 90):.6f} / '
                 f'最大 {np.nanmax(ad):.6f}; 达标(≤1e-2) {int((ad <= POOL_TOL).sum())}/{int(both.sum())} 行; 重算后 NaN {int(w.isna().sum())} 行')
    L.append('\n注: 存值是「买入日 + 全目录」口径 ⇒ 换成 tb 口径后**按定义**必然变动（前视消除的代价），'
             '1e-2 容差**不适用于"新 vs 旧存值"**，只适用于"同一新口径的两个实现之间"（对端独立复核用）。')
    L.append('验证(决定性): 用**同一新口径**但把取值日换成 buy_date，与存值重合到 1e-7 量级'
             '（`chk_pool_stat_variants.py` 的 all_buy 变体; 对端独立复核 zz1000 20260903 实测 Δ=3.8e-07）'
             '⇒ 上表 |Δ| 全部来自 **tb↔buy_date 日错位**，不含实现误差。\n')
    L.append('## ④ NaN 计数\n')
    L.append('| pool | 行数 | 任一 NaN | 原因: 当日无行情文件 | 存疑行(单列) |')
    L.append('|---|---|---|---|---|')
    for p in POOLS:
        s = df[df['pool'] == p]
        L.append(f'| {p} | {len(s)} | {int(s["pool_feat_nan"].sum())} | {int((s["pool_feat_nan_reason"] == "day_no_quote").sum())} | {int(s["tb_uncertain"].sum())} |')
    L.append('')
    L.append('## ⑤ 不可近似还原的日期段（披露）\n')
    for p in POOLS:
        s = stats[stats['pool'] == p].sort_values('trade_date')
        med = int(s['n_codes'].median())
        thin = s[s['n_codes'] < 0.5 * med]
        segs = []
        for r in thin.itertuples():
            if segs and segs[-1][1] >= r.trade_date:
                segs[-1][1] = max(segs[-1][1], r.trade_date)
            else:
                segs.append([r.trade_date, r.trade_date])
        L.append(f'- {p}: 目录首个交易日 {s["trade_date"].min()}, 当日文件数中位 {med}, 最少 {int(s["n_codes"].min())}；'
                 f'文件数 <50% 中位的日期段: {[f"{x[0]}~{x[1]}" for x in segs] or "无"}')
    L.append('- 说明: 目录口径下"名单为空"不再是问题（不读 members.pkl）；上表列出的是**行情覆盖稀疏**的日期段 ——')
    L.append('  这些日子的 n_zt/avg_ret 由当日实际有行情的文件得出，与当时"成分"可能有偏差，回填值已按当日实际计算并留痕。')
    L.append('\n## ⑥ 口径清单（v1 = pool-feat-v1，待 Owner 签批生效）\n')
    L.append('- 成员集 = 该池日线目录当日全部有行情文件（对端实测复核；本端 all_buy 变体独立重合）')
    L.append('- 池级统计日期 = 信号日 tb（裁定 ②），废止买入日口径（消除前视）')
    L.append('- f12 = C(tb)/max(1, mean(C(tb-250..tb-1)))，C 与 scan all_sig 同语义（同 code 多链计多次），不读 baseline ⇒ 幂等')
    L.append('- f13 = n_zt(tb)/20（阈值 9.8）；f14 = avg_ret(tb) 百分数单位')
    L.append('- NaN: 当日无行情文件 → 三特征 NaN（不插值）；f12 分母下限 max(1, ·)')
    open(a.report, 'w').write('\n'.join(L) + '\n')
    print('\n'.join(L))
    print(f'\n报告: {a.report}')


if __name__ == '__main__':
    main()