#!/usr/bin/env python3
"""Detection threshold of the London-breakout test: how small an injected edge can it still see?

Reads the per-year lines written by `calibrate.py --null-years 0 --edge-years N --edge E`
from ~/data/threshold/edge-E.log (one file per edge size), summarises each edge size, writes
`threshold.md` next to this file and an SVG chart for the site.

Certification rate = share of synthetic years whose published statistic (rule mean minus
control mean, in control standard errors) is at or above the kill rule's bar of 2. The null
rate for the same bar, from calibration.md, is 5% (4 of 80 sign-flipped years).

Usage: python threshold.py [--logs ~/data/threshold] [--svg ../../site/public/images/detection-threshold.svg]
"""
import glob
import os
import re
import statistics
import sys
from datetime import datetime, timezone

HERE = os.path.dirname(os.path.abspath(__file__))
NULL_RATE_2 = 0.05      # from calibration.md, 80 null years
NULL_RATE_1_5 = 0.10
LINE = re.compile(r"edge year\s+(\d+): trades\s+(\d+) rule\s+([+-]?[\d.]+) ctrl_se\s+([\d.]+) excess\s+([+-]?[\d.]+) same=(True|False)")


def arg(name, default, cast=str):
    if name in sys.argv:
        return cast(sys.argv[sys.argv.index(name) + 1])
    return default


def read_logs(folder):
    out = {}
    for path in sorted(glob.glob(os.path.join(folder, "edge-*.log"))):
        edge = float(re.search(r"edge-([\d.]+)\.log$", path).group(1))
        rows = []
        with open(path) as f:
            for line in f:
                m = LINE.match(line.strip())
                if m:
                    rows.append({"trades": int(m.group(2)), "rule": float(m.group(3)), "ctrl_se": float(m.group(4)),
                                 "excess": float(m.group(5)), "same": m.group(6) == "True"})
        if rows:
            out[edge] = rows
    return dict(sorted(out.items()))


def summarise(rows):
    ex = [r["excess"] for r in rows]
    n = len(ex)
    return {
        "years": n,
        "trades": statistics.fmean(r["trades"] for r in rows),
        "rule": statistics.fmean(r["rule"] for r in rows),
        "ctrl_se": statistics.fmean(r["ctrl_se"] for r in rows),
        "mean": statistics.fmean(ex),
        "sd": statistics.pstdev(ex) if n > 1 else 0.0,
        "cert2": sum(e >= 2 for e in ex) / n,
        "cert1_5": sum(e >= 1.5 for e in ex) / n,
        "min": min(ex),
        "max": max(ex),
    }


def svg_chart(edges, summary, path):
    """One series, one hue, on the site's dark ground. Hand-written SVG so the site stays free of chart libraries."""
    W, H = 720, 400
    L, R, T, B = 64, 24, 40, 56
    ground, rule, text, dim, accent = "#0f1120", "#262a44", "#e8e3d8", "#a9a2c6", "#f2b544"
    xs = [0.0] + edges
    ys = [NULL_RATE_2] + [summary[e]["cert2"] for e in edges]
    xmax = max(xs) * 1.08
    def X(v): return L + (W - L - R) * v / xmax
    def Y(v): return T + (H - T - B) * (1 - v)
    parts = [f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {W} {H}" width="{W}" height="{H}" role="img" '
             f'aria-labelledby="t d" font-family="JetBrains Mono, ui-monospace, Menlo, monospace" font-size="12">',
             '<title id="t">How often the London-breakout test certifies an injected edge, by edge size</title>',
             f'<desc id="d">Certification rate at the 2 standard-error bar against the size of the injected drift in USD per ounce per minute. '
             f'At zero drift the rate is the null rate of {NULL_RATE_2:.0%}. ' +
             " ".join(f"At {e:g} it is {summary[e]['cert2']:.0%}." for e in edges) + '</desc>',
             f'<rect width="{W}" height="{H}" fill="{ground}"/>']
    # recessive grid
    for v in (0, 0.25, 0.5, 0.75, 1.0):
        parts.append(f'<line x1="{L}" y1="{Y(v):.1f}" x2="{W - R}" y2="{Y(v):.1f}" stroke="{rule}" stroke-width="1"/>')
        parts.append(f'<text x="{L - 8}" y="{Y(v) + 4:.1f}" fill="{dim}" text-anchor="end">{v:.0%}</text>')
    for v in xs:
        parts.append(f'<text x="{X(v):.1f}" y="{H - B + 20}" fill="{dim}" text-anchor="middle">{v:g}</text>')
    parts.append(f'<text x="{(L + W - R) / 2:.1f}" y="{H - 12}" fill="{dim}" text-anchor="middle">injected drift, USD/oz per minute for 120 minutes after the breakout</text>')
    parts.append(f'<text x="{L}" y="{T - 16}" fill="{text}" font-weight="500">share of synthetic years certified at the 2 s.e. bar</text>')
    # null reference
    parts.append(f'<line x1="{L}" y1="{Y(NULL_RATE_2):.1f}" x2="{W - R}" y2="{Y(NULL_RATE_2):.1f}" stroke="{dim}" stroke-width="1" stroke-dasharray="4 4"/>')
    parts.append(f'<text x="{W - R}" y="{Y(NULL_RATE_2) - 6:.1f}" fill="{dim}" text-anchor="end">null rate {NULL_RATE_2:.0%} (80 sign-flipped years)</text>')
    # the series
    pts = " ".join(f"{X(x):.1f},{Y(y):.1f}" for x, y in zip(xs, ys))
    parts.append(f'<polyline points="{pts}" fill="none" stroke="{accent}" stroke-width="2" stroke-linejoin="round"/>')
    for x, y in zip(xs, ys):
        parts.append(f'<circle cx="{X(x):.1f}" cy="{Y(y):.1f}" r="4.5" fill="{accent}" stroke="{ground}" stroke-width="2"/>')
        parts.append(f'<text x="{X(x):.1f}" y="{Y(y) - 12:.1f}" fill="{text}" text-anchor="middle">{y:.0%}</text>')
    parts.append("</svg>")
    with open(path, "w") as f:
        f.write("\n".join(parts) + "\n")


def main():
    logs = os.path.expanduser(arg("--logs", "~/data/threshold"))
    svg = arg("--svg", os.path.join(HERE, "..", "..", "site", "public", "images", "detection-threshold.svg"))
    data = read_logs(logs)
    if not data:
        print("no logs found in", logs)
        return
    summary = {e: summarise(rows) for e, rows in data.items()}
    edges = list(summary)
    lines = [f"# Detection threshold of the London-breakout test — summarised {datetime.now(timezone.utc):%Y-%m-%d %H:%M} UTC", "",
             "This is a test record, not advice.", "",
             "Each row: `calibrate.py --null-years 0 --edge-years 40 --edge E --seed 11`, i.e. sign-flipped 2026 days (no directional structure) "
             "plus a drift of E USD/oz per minute in the breakout direction for 120 minutes after each day's first breakout; the full pipeline "
             "(rule, 300-series random-entry control, halves) unchanged. Null reference from calibration.md: 80 years, 5.0% at ≥2 s.e., 10.0% at ≥1.5 s.e.", "",
             "| drift E (USD/oz/min) | years | mean trades | rule mean, USD/oz per trade | control s.e. | excess mean | excess s.d. | min | max | ≥2 s.e. | ≥1.5 s.e. |",
             "|---|---|---|---|---|---|---|---|---|---|---|",
             f"| 0 (null) | 80 | 105 | | | +0.05 | 1.07 | −1.95 | +2.69 | {NULL_RATE_2:.0%} | {NULL_RATE_1_5:.0%} |"]
    for e in edges:
        s = summary[e]
        lines.append(f"| {e:g} | {s['years']} | {s['trades']:.0f} | {s['rule']:+.2f} | {s['ctrl_se']:.2f} | {s['mean']:+.2f} | {s['sd']:.2f} | "
                     f"{s['min']:+.2f} | {s['max']:+.2f} | {s['cert2']:.0%} | {s['cert1_5']:.0%} |")
    lines += ["", "Standard error of a rate from 40 years is about 8 points near 50% and about 3 points near 5%.", ""]
    text = "\n".join(lines)
    with open(os.path.join(HERE, "threshold.md"), "w") as f:
        f.write(text + "\n")
    print(text)
    svg_chart(edges, summary, svg)
    print("chart:", svg)


if __name__ == "__main__":
    main()
