#!/usr/bin/env python3
"""chk_exdiv_judge_eval.py — 全库评估"除权判据"命中率/误报率(只读)。

真值的阈值很关键: 因子跳幅 j% 对应的价格口径差 ≈ j pp, 故 j < TOL(0.05pp) 的因子微调
**根本没有可测的价格差**(属亚阈噪声, 不必判出)。因此分层报告:
  j > 1e-4 (4e-6 量级) / j > 5e-4 (=0.05pp, 与判据容差同量级) / j > 0.5%(材料性大额)
对比 B-only(定稿 code/exdiv_judge.py) vs A∧B(旧判据)。误报代价: 只在②末行因子兜底路径生效,
后果为拒写等接口恢复 → 安全侧。

用法: python code/chk_exdiv_judge_eval.py [--days 20241018,20230821,20260911]
"""
import sys, glob
import numpy as np
import pandas as pd

BASE = '/Users/xpresso/zt_app/backtest_zt_full'
DAYS = None
if '--days' in sys.argv:
    DAYS = set(sys.argv[sys.argv.index('--days') + 1].split(','))
TOL = 0.05
THRS = [('>0.01%', 0.01), ('>0.05pp(=判据容差)', 0.05), ('>0.5%(材料性)', 0.5)]
FR = sorted(glob.glob(f'{BASE}/daily_*/*.csv'))
acc = {name: dict(truth=0, b_hit=0, ab_hit=0, b_miss=[], ab_miss=[], b_fp=0, non=0) for name, _ in THRS}
acc['ALL'] = dict(truth=0, b_hit=0, ab_hit=0, b_miss=[], ab_miss=[], b_fp=0, non=0)
per_day = {name: {} for name, _ in THRS}
for f in FR:
    d = pd.read_csv(f, dtype={'trade_date': str}).sort_values('trade_date').reset_index(drop=True)
    if len(d) < 3:
        continue
    prev = d['raw_close'].shift(1).values
    fr = d['adj_factor'].values / d['adj_factor'].shift(1).values
    pct = d['pct_chg'].values; rc = d['raw_close'].values; pc = d['pre_close'].values
    B = (rc / prev - 1) * 100
    A = (rc / pc - 1) * 100
    ok = np.isfinite(prev) * np.isfinite(fr) * np.isfinite(pct)
    jump = np.abs(fr - 1) * 100
    jB = np.logical_and(ok, np.logical_not(np.abs(B - pct) < TOL))
    jAB = np.logical_and(ok, np.logical_and(np.abs(A - pct) < TOL, np.abs(B - pct) >= TOL))
    dates = d['trade_date'].values
    for name, thr in THRS:
        t = np.logical_and(ok, jump > thr)
        nn = np.logical_and(ok, jump <= thr)
        a = acc[name]
        a['truth'] += int(t.sum()); a['non'] += int(nn.sum())
        a['b_hit'] += int(np.logical_and(t, jB).sum()); a['ab_hit'] += int(np.logical_and(t, jAB).sum())
        a['b_fp'] += int(np.logical_and(nn, jB).sum())
        a['b_miss'] += list(jump[np.logical_and(t, np.logical_not(jB))])
        a['ab_miss'] += list(jump[np.logical_and(t, np.logical_not(jAB))])
        if DAYS:
            for i in np.where(ok)[0]:
                if dates[i] in DAYS:
                    k = per_day[name].setdefault(dates[i], dict(n=0, truth=0, b=0, ab=0, fp=0))
                    k['n'] += 1; k['truth'] += int(t[i]); k['b'] += int(jB[i]); k['ab'] += int(jAB[i])
                    k['fp'] += int(bool(nn[i]) and bool(jB[i]))
    a = acc['ALL']
    t = ok
    a['truth'] += int(ok.sum())

def pctl(a, q):
    return round(float(np.percentile(a, q)), 4) if len(a) else float('nan')

print(f'库 {len(FR)} 只 | 总可判行 {acc["ALL"]["truth"]}')
print(f'{"真值阈值":<16}{"真除权":>9}{"B-only命中":>12}{"A∧B命中":>10}{"B漏≥0.5%":>10}{"A∧B漏≥0.5%":>12}'
      f'{"B漏max":>10}{"B误报率":>10}')
for name, _ in THRS:
    a = acc[name]
    print(f'{name:<16}{a["truth"]:>9}{a["b_hit"]:>7}({a["b_hit"]/max(a["truth"],1)*100:.1f}%)'
          f'{a["ab_hit"]:>6}({a["ab_hit"]/max(a["truth"],1)*100:.1f}%)'
          f'{sum(1 for x in a["b_miss"] if x>=0.5):>10}{sum(1 for x in a["ab_miss"] if x>=0.5):>12}'
          f'{pctl(a["b_miss"],100):>10}{a["b_fp"]/max(a["non"],1)*100:>9.3f}%')
print()
for name, _ in THRS:
    print(f'--- 真值 {name} 的单日明细 ---')
    for t, k in sorted(per_day[name].items()):
        print(f'  {t}: 比对 {k["n"]} 只 | 真除权 {k["truth"]} | A∧B 命中 {k["ab"]} | '
              f'B-only 命中 {k["b"]} | B-only 误报 {k["fp"]} ({k["fp"]/max(k["n"],1)*100:.3f}%)')