#!/usr/bin/env python3
"""键脆弱度分析（Mac 侧）：为每个 (code,buy_date) 键算出"最紧约束余量"，
用于跨机键集合差异（对端 5786 vs 本端 5776: 11/1）的归因。

做法: 对每个键复走 chain_for_buy 的判定链，记录各约束的**相对余量**:
  m_t0    = pct[t0] - (涨停阈值 - 0.05)                 非负即为 t0 成立（0 附近 = 脆弱）
  m_tb    = close[tb]/(p0*(1+bs)) - 1                   >0 即突破成立
  m_ta    = (次低合格 ta 的 close - 选中 close)/选中 close   越小 = ta 选择越不稳（数据微动即换日）
  m_vol   = 各量能过滤(t0_vol_min/max, tb_vol_min/max, shrink)的归一化余量最小者
  m_min   = 上述最小值（越小越脆弱）
输出: --out csv（code,pool,buy_date,tb,t0,m_t0,m_tb,m_ta,m_vol,m_min）+ 脆弱榜 topN 打印

用法: python chk_key_fragility.py --out /tmp/key_fragility.csv [--top 25]
"""
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, SELL_COMBO
from plot_ideal_top10 import chain_for_buy


def margins(d, code, bp, buy_date):
    d = d.sort_values('trade_date').reset_index(drop=True)
    pct = d['pct_chg'].astype(float).values
    close = d['hfq_close'].astype(float).values
    vol = d['vol'].astype(float).values
    dates = d['trade_date'].values
    if code.startswith(('60', '00')):
        bt = 9.9
    elif code.startswith(('30', '68')):
        bt = 19.9
    else:
        bt = 29.9
    vb, vs, bs = bp['vol_base'], bp['vol_shrink'], bp['break_strength']
    sw, bu, mn = int(bp['shrink_win']), int(bp['break_up']), int(bp['ma_n'])
    ztv = bp.get('t0_vol_max', bp.get('zt_vol_filter', 0))
    t0min = bp.get('t0_vol_min', 0.0)
    tbt0_min = bp.get('tb_t0_min', 0.0)
    tbt0_max = bp.get('tb_t0_max', 0.0)
    tbmin = bp.get('tb_vol_min', 0.0)
    tbmax = bp.get('tb_vol_max', 0.0)
    t0i, tai, tbi = chain_for_buy(d, code, bp, str(buy_date))
    if t0i is None:
        return None
    p0 = close[t0i]
    v0 = vol[t0i]
    m_t0 = float(pct[t0i] - (bt - 0.05))
    m_tb = float(close[tbi] / (p0 * (1 + bs)) - 1)
    # ta 选择稳定性: 窗口内合格缩量日按 close 排序，取选中与次低之差
    cands = []
    for j in range(t0i + 1, min(t0i + sw + 1, len(d))):
        shrink_ok = False
        if vb in ('zt', 'or') and vol[j] < v0 * vs:
            shrink_ok = True
        if vb in ('maN', 'or'):
            m = vol[max(0, j - mn):j].mean() if j >= mn else vol[:j].mean()
            if m > 0 and vol[j] < m * vs:
                shrink_ok = True
        if shrink_ok and close[j] < p0:
            cands.append((close[j], j))
    cands.sort()
    if len(cands) >= 2:
        m_ta = float((cands[1][0] - cands[0][0]) / cands[0][0])
    else:
        m_ta = np.nan   # 唯一候选
    vols = []
    if v0 > 0:
        ma10z = vol[max(0, t0i - 10):t0i].mean() if t0i >= 10 else vol[:t0i].mean()
        if t0min > 0 and ma10z > 0:
            vols.append(v0 / (ma10z * t0min) - 1)
        if ztv > 0 and ma10z > 0:
            vols.append(1 - v0 / (ma10z * ztv))
        cb = vol[t0i + 1:tbi].mean() if tbi > t0i + 1 else vol[tbi]
        if tbt0_min > 0:
            vols.append(vol[tbi] / (v0 * tbt0_min) - 1)
        if tbt0_max > 0:
            vols.append(1 - vol[tbi] / (v0 * tbt0_max))
        if tbmin > 0 and cb > 0:
            vols.append(vol[tbi] / (cb * tbmin) - 1)
        if tbmax > 0 and cb > 0:
            vols.append(1 - vol[tbi] / (cb * tbmax))
        # ta 缩量余量
        vols.append(1 - vol[tai] / (v0 * vs) if vb == 'zt' else 1 - vol[tai] / (vol[max(0, tai - mn):tai].mean() * vs))
    m_vol = float(np.nanmin(vols)) if vols else np.nan
    arr = [x for x in [m_t0, m_tb, m_ta, m_vol] if x is not None and not (isinstance(x, float) and np.isnan(x))]
    return {'tb': str(dates[tbi]), 't0': str(dates[t0i]), 'm_t0': m_t0, 'm_tb': m_tb, 'm_ta': m_ta,
            'm_vol': m_vol, 'm_min': float(min(arr)) if arr else np.nan}


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument('--out', default='/tmp/key_fragility.csv')
    ap.add_argument('--top', type=int, default=25)
    ap.add_argument('--keys-from', default=None, help='只分析该 csv 里的键(code,buy_date[,pool])')
    a = ap.parse_args()
    import ideal_engine as ie
    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:
            m = margins(dailies[t['code']], t['code'], bp, t['buy_date'])
            if m is None:
                continue
            rows.append({'code': t['code'], 'pool': pool, 'buy_date': str(t['buy_date']), **m})
        print(f'{pool}: {len(rows)} 键已算余量', flush=True)
    df = pd.DataFrame(rows).sort_values('m_min')
    df.to_csv(a.out, index=False)
    print(f'\n共 {len(df)} 键 -> {a.out}')
    print('\n最脆弱 25 键（m_min 越小越易因跨机数据微动而增减）:')
    print(df.head(a.top).to_string(index=False))


if __name__ == '__main__':
    main()