#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""回测口径审计（只读）：池子构成 / 一字判定 / 策略样本外 / 随机对照
跑在 s55：python3 /tmp/audit_all.py > /tmp/audit_all.txt
"""
import glob, json, sqlite3, statistics, random
from collections import defaultdict

DB = "/root/px_history.db"
FEE = 0.12
random.seed(20261107)

con = sqlite3.connect("file:%s?mode=ro" % DB, uri=True)
px = {}
by_code = defaultdict(list)
for d, ts, o, cl, pc, h, l in con.execute(
        "SELECT trade_date, ts_code, open, close, pre_close, high, low FROM daily"):
    if not o or not cl or not pc:
        continue
    px[(d, ts)] = (o, cl, pc, h, l)
    by_code[ts].append(d)
for ts in by_code:
    by_code[ts].sort()
pos = {ts: {d: i for i, d in enumerate(v)} for ts, v in by_code.items()}
days = sorted({d for d, _ in px})
nxt = {days[i]: days[i + 1] for i in range(len(days) - 1)}
print("== 日线库：%d 天 %s ~ %s，%d 行，%d 只" % (len(days), days[0], days[-1], len(px), len(by_code)), flush=True)

def L(code):
    if code.startswith(("300", "301", "688", "689")):
        return 20.0
    if code.startswith(("8", "4", "920")):
        return 30.0
    return 10.0

def suf(c):
    if c.startswith(("60", "68", "90")):
        return "SH"
    if c.startswith(("00", "30", "20")):
        return "SZ"
    return "BJ"

def up_px(pc, l):
    return round(pc * (1 + l / 100.0) + 1e-9, 2)

def dn_px(pc, l):
    return round(pc * (1 - l / 100.0) + 1e-9, 2)

# ---------- 1. 池子构成 ----------
files = sorted(glob.glob("/opt/kpl-cli/data/auction_hist/20*.json"))
rows_n, valid_days, per_day = 0, 0, []
pool = []
for path in files:
    try:
        d = json.load(open(path, encoding="utf-8"))
    except Exception:
        continue
    day = (d.get("day") or "").replace("-", "")
    rs = d.get("rows") or []
    if not rs:
        continue
    valid_days += 1
    rows_n += len(rs)
    per_day.append(len(rs))
    for r in rs:
        c = str(r.get("code") or "")
        if len(c) != 6 or not c.isdigit():
            continue
        pool.append(dict(day=day, code=c, name=str(r.get("name") or ""),
                         ap=r.get("auction_pct"), bid=float(r.get("limit_bid") or 0),
                         net=r.get("auction_net"), turn=r.get("auction_turn"),
                         board=r.get("board"), plate=r.get("plate_name")))
print("\n== 1. 竞价池构成")
print("  文件 %d 个，有数据 %d 天，总行 %d，日均 %.1f 行（min %d / max %d）"
      % (len(files), valid_days, rows_n, rows_n / max(valid_days, 1), min(per_day), max(per_day)))
mkt = defaultdict(int)
for (d, ts) in px:
    mkt[d] += 1
print("  同期全市场日均 %.0f 只 → 池子覆盖率约 %.1f%%"
      % (statistics.mean(mkt.values()), rows_n / max(valid_days, 1) / statistics.mean(mkt.values()) * 100))

# ---------- 2. 一字判定：9.7 阈值 vs 真实涨停价 ----------
print("\n== 2. 「竞价涨幅≥9.7 即一字」这条规则的实际效果")
cnt = defaultdict(int)
ex_miss, ex_wrong = [], []
for r in pool:
    if r["ap"] is None:
        continue
    ts = r["code"] + "." + suf(r["code"])
    t = px.get((r["day"], ts))
    if not t:
        continue
    o, cl, pc0, h0, l0 = t
    l = L(r["code"])
    up = up_px(pc0, l)
    yizi = abs(o - up) < 0.005
    ap_ge = float(r["ap"]) >= 9.7
    kind = "%dcm" % int(l)
    if yizi and ap_ge:
        cnt[(kind, "命中剔除")] += 1
    elif yizi and not ap_ge:
        cnt[(kind, "漏剔(买不进却计入)")] += 1
        ex_miss.append((r["day"], r["code"], r["name"], r["ap"], l))
    elif (not yizi) and ap_ge:
        cnt[(kind, "误剔(能买却排除)")] += 1
        ex_wrong.append((r["day"], r["code"], r["name"], r["ap"], l))
    else:
        cnt[(kind, "正常保留")] += 1
for kind in ("10cm", "20cm", "30cm"):
    tot = sum(v for (k, _), v in cnt.items() if k == kind)
    if not tot:
        continue
    print("  %s（%d 笔）：" % (kind, tot))
    for lab in ("命中剔除", "漏剔(买不进却计入)", "误剔(能买却排除)", "正常保留"):
        v = cnt[(kind, lab)]
        print("    %-16s %5d 笔 %5.1f%%" % (lab, v, v / tot * 100))
print("  漏剔示例：", ex_miss[:5])
print("  误剔示例：", ex_wrong[:5])

# ---------- 3. 跌停策略：全期 / 分时段 / 随机对照 ----------
print("\n== 3. 竞价跌停买入（1000-1500 或 2000-3000 万，T+1 收盘卖）")
dn_pool = []
for r in pool:
    if r["ap"] is None or r["bid"] < 5e6:
        continue
    ts = r["code"] + "." + suf(r["code"])
    t = px.get((r["day"], ts))
    if not t:
        continue
    o, cl, pc0, h0, l0 = t
    l = L(r["code"])
    if abs(o - dn_px(pc0, l)) > 0.005:          # 开盘即跌停
        continue
    if "ST" in r["name"].upper():
        continue
    d1 = nxt.get(r["day"])
    p1 = px.get((d1, ts)) if d1 else None
    if not p1:
        continue
    o1, c1, pc1, h1, l1 = p1
    f = pc1 / cl if cl and abs(pc1 / cl - 1) > 0.002 else 1.0
    dn1 = dn_px(pc1, l)
    stuck = bool(abs(h1 - l1) < 0.005 and abs(c1 - dn1) < 0.005)
    dn_pool.append(dict(day=r["day"], code=r["code"], bid=r["bid"], l=l, stuck=stuck,
                        ret=(c1 / f / o - 1) * 100 - FEE,
                        ret_o=(o1 / f / o - 1) * 100 - FEE,
                        ret_h=(h1 / f / o - 1) * 100 - FEE))

def stat(v):
    if not v:
        return None
    return dict(n=len(v), win=sum(1 for x in v if x > 0) / len(v) * 100,
                avg=sum(v) / len(v), med=statistics.median(v))

def band(rs):
    return [x for x in rs if (1e7 <= x["bid"] < 1.5e7) or (2e7 <= x["bid"] < 3e7)]

tgt = [x for x in band(dn_pool) if not x["stuck"]]
v = [x["ret"] for x in tgt]
s = stat(v)
print("  全期：%d 笔｜胜率 %.2f%%｜平均 %+.2f%%｜中位 %+.2f%%" % (s["n"], s["win"], s["avg"], s["med"]))
half = days[len(days) // 2]
for lab, sub in (("前半（至 %s）" % half, [x for x in tgt if x["day"] < half]),
                 ("后半（%s 起）" % half, [x for x in tgt if x["day"] >= half])):
    x = stat([i["ret"] for i in sub])
    if x:
        print("  %-16s %2d 笔｜胜率 %5.2f%%｜平均 %+6.2f%%｜中位 %+6.2f%%"
              % (lab, x["n"], x["win"], x["avg"], x["med"]))
for year in ("2024", "2025", "2026"):
    x = stat([i["ret"] for i in tgt if i["day"][:4] == year])
    if x:
        print("  %s：%2d 笔｜胜率 %5.2f%%｜平均 %+6.2f%%" % (year, x["n"], x["win"], x["avg"]))
by = defaultdict(list)
for i in tgt:
    by[int(i["l"])].append(i["ret"])
for l in sorted(by):
    x = stat(by[l])
    print("  %dcm：%2d 笔｜胜率 %5.2f%%｜平均 %+6.2f%%" % (l, x["n"], x["win"], x["avg"]))

# 随机对照：从全部跌停样本里随机抽同样笔数
allret = [x["ret"] for x in dn_pool if not x["stuck"]]
n = len(tgt)
means = []
for _ in range(3000):
    samp = random.sample(allret, min(n, len(allret)))
    means.append(sum(samp) / len(samp))
means.sort()
better = sum(1 for m in means if m >= s["avg"]) / len(means) * 100
print("  随机对照：全部跌停样本 %d 笔（均值 %+.2f%%），随机抽 %d 笔 3000 次 → 均值分布 中位 %+.2f%%、95 分位 %+.2f%%"
      % (len(allret), sum(allret) / len(allret), n, means[len(means) // 2], means[int(len(means) * 0.95)]))
print("  本口径均值 %+.2f%% 在随机分布中排前 %.1f%%（越小越好，<5%% 才算真有信号）" % (s["avg"], better))

# ---------- 4. 三板微高开：按真实涨停价重算 ----------
print("\n== 4. 三板微高开（竞价 0~1% & 委买≥5000 万 → T+1 收盘卖）")
lu = {}
for (d, ts), (o, cl, pc0, h0, l0) in px.items():
    l = L(ts.split(".")[0])
    if cl >= up_px(pc0, l) - 0.005:
        lu[(d, ts)] = True

res = []
for r in pool:
    if r["ap"] is None or r["bid"] < 5e6:
        continue
    ap = float(r["ap"])
    if not (0 < ap < 1):
        continue
    ts = r["code"] + "." + suf(r["code"])
    t = px.get((r["day"], ts))
    if not t or "ST" in r["name"].upper():
        continue
    o, cl, pc0, h0, l0 = t
    l = L(r["code"])
    if abs(o - up_px(pc0, l)) < 0.005:      # 竞价即涨停，买不进
        continue
    i = pos[ts].get(r["day"])
    if i is None or i < 2:
        continue
    d1_, d2_ = by_code[ts][i - 1], by_code[ts][i - 2]
    if not (lu.get((d1_, ts)) and lu.get((d2_, ts))):
        continue
    d1 = nxt.get(r["day"])
    p1 = px.get((d1, ts)) if d1 else None
    if not p1:
        continue
    o1, c1, pc1, h1, l1 = p1
    f = pc1 / cl if cl and abs(pc1 / cl - 1) > 0.002 else 1.0
    stuck = bool(abs(h1 - l1) < 0.005 and abs(c1 - dn_px(pc1, l)) < 0.005)
    res.append(dict(day=r["day"], code=r["code"], l=l, stuck=stuck,
                    ret=(c1 / f / o - 1) * 100 - FEE,
                    ret_o=(o1 / f / o - 1) * 100 - FEE,
                    ret_h=(h1 / f / o - 1) * 100 - FEE))
ok = [x for x in res if not x["stuck"]]
s = stat([x["ret"] for x in ok])
if s:
    print("  全期：%d 笔（剔除 T+1 跌停一字 %d 笔）｜胜率 %.2f%%｜平均 %+.2f%%｜中位 %+.2f%%"
          % (s["n"], len(res) - len(ok), s["win"], s["avg"], s["med"]))
    for lab, k in (("T+1 开盘卖", "ret_o"), ("T+1 收盘卖", "ret"), ("T+1 最高(上限)", "ret_h")):
        x = stat([i[k] for i in ok])
        print("    %-12s 胜率 %5.2f%%｜平均 %+6.2f%%｜中位 %+6.2f%%" % (lab, x["win"], x["avg"], x["med"]))
    for year in ("2024", "2025", "2026"):
        x = stat([i["ret"] for i in ok if i["day"][:4] == year])
        if x:
            print("    %s：%2d 笔｜胜率 %5.2f%%｜平均 %+6.2f%%" % (year, x["n"], x["win"], x["avg"]))

# ---------- 5. 两策略重叠 ----------
print("\n== 5. 交集检查")
dn_set = {(x["day"], x["code"]) for x in dn_pool}
sb_set = {(x["day"], x["code"]) for x in ok}
print("  跌停样本 %d 笔 / 三板微高开 %d 笔 / 重叠 %d 笔" % (len(dn_set), len(sb_set), len(dn_set & sb_set)))
print("\nDONE", flush=True)
