#!/usr/bin/env python3
"""London open breakout on XAUUSD versus a random-entry control. Standard library only.

Rule (fixed before any data was seen; see the experiments entry of 2026-09-05):
  range   = high/low of bid-ask mid, 00:00-06:59 UTC
  entry   = first 1-minute mid close above range high (long, fill next bar's ask open)
            or below range low (short, fill next bar's bid open), within 07:00-09:59 UTC,
            first breakout only, at most one trade per day
  exit    = target and stop both 0.5 * range away, checked on bid (long) / ask (short);
            if both are hit inside one bar the stop is assumed; open trades close at the 16:00 bar
  control = per traded day, a random direction and random entry minute in 07:00-09:59,
            same barriers, same time exit, same spread; N series, mean and s.e. across series

Usage: python3 backtest.py [~/data/bars/XAUUSD_m1.csv] [--series 1000] [--seed 1]
Writes result.md next to this file. A day is used only if the range window has >= 300 bars
and the day has a bar at or after 16:00 (coverage check, since data arrives in pieces).
"""
import csv
import os
import random
import statistics
import sys
from collections import defaultdict
from datetime import datetime, time, timedelta, timezone

HERE = os.path.dirname(os.path.abspath(__file__))
BARRIER_FRAC = 0.5
RANGE_END, ENTRY_START, ENTRY_END, CLOSE_AT = 7 * 60, 7 * 60, 10 * 60, 16 * 60  # minutes of day


def load_days(path):
    days = defaultdict(list)
    with open(path) as f:
        for r in csv.DictReader(f):
            t = datetime.fromisoformat(r["minute"])
            days[t.date()].append((t.hour * 60 + t.minute, *[float(r[k]) for k in (
                "bid_open", "bid_high", "bid_low", "bid_close", "ask_open", "ask_high", "ask_low", "ask_close")]))
    return dict(sorted(days.items()))


def simulate(bars, i, direction, barrier):
    """Enter at bar i's open, return net P&L per ounce, exit reason, exit bar index and exit price.

    The P&L and the reason are the only things the test uses; the exit bar and price are recorded
    for the trade log. A barrier exit is priced at the barrier level, which is what the P&L assumes.
    """
    m, bo, bh, bl, bc, ao, ah, al, ac = bars[i]
    if direction > 0:
        entry = ao
        for j in range(i, len(bars)):
            m, bo, bh, bl, bc, ao, ah, al, ac = bars[j]
            if m >= CLOSE_AT:
                return bo - entry, "time", j, bo
            if bl <= entry - barrier:
                return -barrier, "stop", j, entry - barrier
            if bh >= entry + barrier:
                return barrier, "target", j, entry + barrier
        return bars[-1][4] - entry, "time", len(bars) - 1, bars[-1][4]
    entry = bo
    for j in range(i, len(bars)):
        m, bo, bh, bl, bc, ao, ah, al, ac = bars[j]
        if m >= CLOSE_AT:
            return entry - ao, "time", j, ao
        if ah >= entry + barrier:
            return -barrier, "stop", j, entry + barrier
        if al <= entry - barrier:
            return barrier, "target", j, entry - barrier
    return entry - bars[-1][8], "time", len(bars) - 1, bars[-1][8]


def run(path, n_series, seed):
    rng = random.Random(seed)
    trades = []  # (date, direction, pnl, reason, barrier, window_indices)
    skipped = 0
    for day, bars in load_days(path).items():
        rng_bars = [b for b in bars if b[0] < RANGE_END]
        if len(rng_bars) < 300 or bars[-1][0] < CLOSE_AT:
            skipped += 1
            continue
        rh = max((b[2] + b[6]) / 2 for b in rng_bars)
        rl = min((b[3] + b[7]) / 2 for b in rng_bars)
        barrier = BARRIER_FRAC * (rh - rl)
        window = [i for i, b in enumerate(bars) if ENTRY_START <= b[0] < ENTRY_END and i + 1 < len(bars)]
        signal = None
        for i in window:
            mid_close = (bars[i][4] + bars[i][8]) / 2
            if mid_close > rh:
                signal = (i + 1, 1)
                break
            if mid_close < rl:
                signal = (i + 1, -1)
                break
        if signal is None:
            continue
        pnl, reason, exit_i, exit_price = simulate(bars, signal[0], signal[1], barrier)
        entry_price = bars[signal[0]][5 if signal[1] > 0 else 1]
        trades.append((day, signal[1], pnl, reason, barrier, bars, [i + 1 for i in window],
                       bars[signal[0] - 1][0], bars[signal[0]][0], entry_price,
                       bars[exit_i][0], exit_price))

    def summarise(sub):
        if not sub:
            return None
        rule_mean = statistics.fmean(t[2] for t in sub)
        series_means = []
        for _ in range(n_series):
            tot = 0.0
            for day, d, pnl, reason, barrier, bars, idx, *_ in sub:
                tot += simulate(bars, rng.choice(idx), rng.choice((1, -1)), barrier)[0]
            series_means.append(tot / len(sub))
        c_mean = statistics.fmean(series_means)
        c_se = statistics.pstdev(series_means)
        return {
            "trades": len(sub),
            "longs": sum(t[1] > 0 for t in sub),
            "targets": sum(t[3] == "target" for t in sub),
            "stops": sum(t[3] == "stop" for t in sub),
            "timeouts": sum(t[3] == "time" for t in sub),
            "rule_mean": rule_mean,
            "rule_total": sum(t[2] for t in sub),
            "control_mean": c_mean,
            "control_se": c_se,
            "excess_se": (rule_mean - c_mean) / c_se if c_se else float("nan"),
            "mean_barrier": statistics.fmean(t[4] for t in sub),
        }

    halves = {
        "all": trades,
        "jan-apr": [t for t in trades if t[0].month <= 4],
        "may-aug": [t for t in trades if t[0].month >= 5],
    }
    return {k: summarise(v) for k, v in halves.items()}, skipped, trades


def stamp(day, minute):
    """UTC ISO 8601 timestamp of the bar that opens at `minute` minutes past midnight on `day`."""
    return (datetime.combine(day, time(), timezone.utc) + timedelta(minutes=minute)).isoformat()


def write_trades_csv(trades):
    """One row per rule trade, for trade-by-trade comparison. Writes trades.csv next to this file."""
    with open(os.path.join(HERE, "trades.csv"), "w", newline="") as f:
        w = csv.writer(f)
        w.writerow(["day", "direction", "signal_bar_utc", "entry_fill_utc", "entry_price",
                    "exit_utc", "exit_price", "exit_reason", "pnl"])
        for day, d, pnl, reason, barrier, bars, idx, sig_m, fill_m, entry_price, exit_m, exit_price in trades:
            w.writerow([day.isoformat(), "long" if d > 0 else "short", stamp(day, sig_m),
                        stamp(day, fill_m), f"{entry_price:.3f}", stamp(day, exit_m),
                        f"{exit_price:.3f}", reason, f"{pnl:+.2f}"])


def main():
    argv = sys.argv[1:]
    args = [a for i, a in enumerate(argv) if not a.startswith("--") and (i == 0 or not argv[i - 1].startswith("--"))]
    path = args[0] if args else os.path.expanduser("~/data/bars/XAUUSD_m1.csv")
    n_series = int(sys.argv[sys.argv.index("--series") + 1]) if "--series" in sys.argv else 1000
    seed = int(sys.argv[sys.argv.index("--seed") + 1]) if "--seed" in sys.argv else 1
    res, skipped, trades = run(path, n_series, seed)
    lines = [f"# London breakout on XAUUSD — run {datetime.now(timezone.utc):%Y-%m-%d %H:%M} UTC",
             "", "This is a test record, not advice. Do not trade on it.", "",
             f"Data: `{path}`. Days skipped for incomplete coverage: {skipped}. "
             f"Control series: {n_series}, seed {seed}. P&L in USD per ounce, after the tick spread.", "",
             "| window | trades | longs | target / stop / time | rule mean | rule total | control mean | control s.e. | excess (s.e.) |",
             "|---|---|---|---|---|---|---|---|---|"]
    for k, s in res.items():
        if s is None:
            lines.append(f"| {k} | 0 | | | | | | | |")
            continue
        lines.append(f"| {k} | {s['trades']} | {s['longs']} | {s['targets']} / {s['stops']} / {s['timeouts']} | "
                     f"{s['rule_mean']:+.2f} | {s['rule_total']:+.1f} | {s['control_mean']:+.2f} | "
                     f"{s['control_se']:.2f} | {s['excess_se']:+.1f} |")
    if res["all"]:
        lines += ["", f"Mean barrier (0.5 × range): {res['all']['mean_barrier']:.2f} USD/oz."]
    lines += ["", "## Trades", "", "| date | dir | exit | pnl |", "|---|---|---|---|"]
    lines += [f"| {t[0]} | {'long' if t[1] > 0 else 'short'} | {t[3]} | {t[2]:+.2f} |" for t in trades]
    text = "\n".join(lines) + "\n"
    with open(os.path.join(HERE, "result.md"), "w") as f:
        f.write(text)
    write_trades_csv(trades)
    print("\n".join(lines[: 12 + len(res)]))


if __name__ == "__main__":
    main()
