"""
Years-long history check from 1hr candles only — no tick data needed.

Answers the one question that decides whether this strategy can pay: how far does
price travel from a line before it comes back and touches it? That distance, in
0.1% bricks, is what one cycle is worth at 75x (before fees and before the
chart-3 flips).

  python history.py                 # ZECUSDT, from the oldest hour Binance has
  python history.py --from 2025-01-01

Method: build the 0.1% Renko from hourly closes, take each brick's start as a
line, then walk forward to the first hour whose high/low range touches that price
again. Candles are coarse, so a touch inside an hour is counted at the close of
that hour — good for a distribution, not for cent-level fills.
"""
import argparse
import json
import statistics
import urllib.request
from collections import Counter
from datetime import datetime, timezone
from decimal import Decimal as D

FAPI = "https://fapi.binance.com/fapi/v1"


def klines(symbol, start_ms, end_ms=None):
    url = (f"{FAPI}/klines?symbol={symbol}&interval=1h&startTime={start_ms}"
           f"&limit=1500" + (f"&endTime={end_ms}" if end_ms else ""))
    with urllib.request.urlopen(url, timeout=30) as r:
        return json.load(r)


def fetch_all(symbol, start_ms):
    out, cursor, now_ms = [], start_ms, int(datetime.now(timezone.utc).timestamp() * 1000)
    while cursor < now_ms:
        batch = klines(symbol, cursor)
        batch = [k for k in batch if int(k[0]) < cursor + 1500 * 3_600_000]
        if not batch:
            break
        out += batch
        cursor = int(batch[-1][0]) + 3_600_000
        print(f"\r  {len(out)} hours ...", end="", flush=True)
    print()
    return [k for k in out if int(k[6]) < now_ms]        # closed candles only


def renko(closes, pct):
    """Same engine as the bot, fed hourly closes. Returns [(open, close, dir, candle_index)]."""
    bricks, o = [], None
    close, d = None, 0
    for idx, p in enumerate(closes):
        if close is None:
            o = close = p
            continue
        while True:
            if d >= 0 and p >= close + close * pct:
                new = (close, close + close * pct, 1)
            elif d <= 0 and p <= close - close * pct:
                new = (close, close - close * pct, -1)
            elif d == 1 and p <= o - o * pct:
                new = (o, o - o * pct, -1)
            elif d == -1 and p >= o + o * pct:
                new = (o, o + o * pct, 1)
            else:
                break
            o, close, d = new
            bricks.append((*new, idx))
    return bricks


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--symbol", default="ZECUSDT")
    ap.add_argument("--pct", type=float, default=0.001)
    ap.add_argument("--from", dest="start", default="2020-01-01")
    a = ap.parse_args()

    start = int(datetime.strptime(a.start, "%Y-%m-%d")
                .replace(tzinfo=timezone.utc).timestamp() * 1000)
    print(f"fetching {a.symbol} 1h candles from {a.start}")
    kl = fetch_all(a.symbol, start)
    if len(kl) < 100:
        raise SystemExit(f"only {len(kl)} candles — is {a.symbol} listed that far back?")
    first = datetime.fromtimestamp(int(kl[0][0]) / 1000, timezone.utc).strftime("%Y-%m-%d")
    last = datetime.fromtimestamp(int(kl[-1][0]) / 1000, timezone.utc).strftime("%Y-%m-%d")

    closes = [D(k[4]) for k in kl]
    highs = [D(k[2]) for k in kl]
    lows = [D(k[3]) for k in kl]
    bricks = renko(closes, D(str(a.pct)))

    # run lengths: consecutive bricks in the same direction
    runs, cur, lastd = [], 0, 0
    for _, _, d, _ in bricks:
        if d == lastd:
            cur += 1
        else:
            if cur:
                runs.append(cur)
            cur, lastd = 1, d
    if cur:
        runs.append(cur)

    # one cycle = from a line (brick start) to the first hour that touches it again
    pct = D(str(a.pct))
    cycles = []
    for line, _c, _d, t0 in bricks:
        hit = None
        for j in range(t0 + 1, min(len(kl), t0 + 1000)):      # ~6 weeks to come back
            if lows[j] <= line <= highs[j]:
                hit = j
                break
        if hit is not None:
            cycles.append(float(abs(closes[hit] - line) / (line * pct)))
    candles = len(kl)
    span_days = (int(kl[-1][0]) - int(kl[0][0])) / 86_400_000

    print(f"\n=== {a.symbol} · 0.1% Renko built from hourly closes ===")
    print(f"{first} -> {last} | {candles:,} hours ({span_days:.0f} days) | "
          f"{len(bricks):,} bricks | {len(bricks) / max(span_days, 1):.2f} bricks/day")
    if runs:
        print(f"runs of 1hr bricks: n={len(runs)} | longest {max(runs)} | "
              f"median {statistics.median(runs):.0f} | mean {statistics.mean(runs):.2f}")
        tally = Counter()
        for r in runs:
            tally["1" if r == 1 else "2" if r == 2 else "3-4" if r < 5
                  else "5-9" if r < 10 else "10-19" if r < 20
                  else "20-49" if r < 50 else "50+"] += 1
        order = ["1", "2", "3-4", "5-9", "10-19", "20-49", "50+"]
        print("  distribution:", " | ".join(f"{k}: {tally[k]} ({tally[k] / len(runs) * 100:.1f}%)"
                                            for k in order if tally[k]))
    if cycles:
        cyc = sorted(cycles)
        def pct(p):
            return cyc[min(len(cyc) - 1, int(len(cyc) * p))]
        print(f"bricks from a line back to that line: n={len(cycles)} | "
              f"median {statistics.median(cycles):.1f} | mean {statistics.mean(cycles):.1f} | "
              f"p25 {pct(.25):.1f} | p75 {pct(.75):.1f} | p90 {pct(.9):.1f} | max {max(cycles):.1f}")
        for edge in (1, 2, 3, 5, 10):
            n = sum(1 for x in cycles if x >= edge)
            print(f"  cycles worth >= {edge} brick(s): {n} ({n / len(cycles) * 100:.0f}%)")


if __name__ == "__main__":
    main()
