"""The monkey test: does a popular rule time its entries better than random entries with the same trades and holding times?

python3 -I tools/monkey.py data [FEE_PER_SIDE] [FROM] [TO]  -> one table per coin, and data/monkey_<fee>_<from>_<to>.json
(the candles: data/<SYMBOL>_4h.json.gz from tools/fetch.py, Binance spot via data-api.binance.vision; the last bar used is
the last closed one before TO)

Rules (long only, one position, no stop): a signal on a bar's close fills at the next bar's open; fees on both sides.
A monkey keeps the strategy's number of trades and its holding times (shuffled) and places them at random, without
overlap, inside the same window: only the timing differs. 1,000 monkeys per strategy and coin, seed 7.
Indicators follow TradingView's definitions: EMA (alpha 2/(n+1)), RSI on Wilder's average, MACD 12/26/9, ta.supertrend
(ATR on Wilder's average), Turtle and Connors on the prior bars' highs/lows and SMAs.
"""
import gzip, json, os, sys
import numpy as np, pandas as pd

D = sys.argv[1]; FEE = float(sys.argv[2]) if len(sys.argv) > 2 else 0.001
A = sys.argv[3] if len(sys.argv) > 3 else "2018-01-01"; Z = sys.argv[4] if len(sys.argv) > 4 else "2026-10-25"
COINS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "LTCUSDT", "XRPUSDT"]; M = 1000

def ema(x, n): return pd.Series(x).ewm(span=n, adjust=False).mean().values
def rma(x, n): return pd.Series(x).ewm(alpha=1 / n, adjust=False).mean().values
def sma(x, n): return pd.Series(x).rolling(n).mean().values
def rsi(c, n):
    d = np.diff(c, prepend=c[0]); up, dn = rma(np.clip(d, 0, None), n), rma(np.clip(-d, 0, None), n)
    with np.errstate(divide="ignore", invalid="ignore"): return np.where(dn == 0, 100.0, 100 - 100 / (1 + up / dn))
def cross(a, b):   # a crosses above b on this bar
    x = np.zeros(len(a), bool); x[1:] = (a[1:] > b[1:]) & (a[:-1] <= b[:-1]); return x
def supertrend_up(h, l, c, n=10, f=3.0):   # ta.supertrend: True while the trend is up
    pc = np.concatenate([[c[0]], c[:-1]]); tr = np.maximum(h - l, np.maximum(abs(h - pc), abs(l - pc))); atr = rma(tr, n)
    hl2 = (h + l) / 2; ub, lb = hl2 + f * atr, hl2 - f * atr; up = np.zeros(len(c), bool); st_is_upper = True
    for i in range(1, len(c)):
        lb[i] = lb[i] if (lb[i] > lb[i - 1] or c[i - 1] < lb[i - 1]) else lb[i - 1]
        ub[i] = ub[i] if (ub[i] < ub[i - 1] or c[i - 1] > ub[i - 1]) else ub[i - 1]
        if st_is_upper: up[i] = c[i] > ub[i]
        else: up[i] = not (c[i] < lb[i])
        st_is_upper = not up[i]
    return up

def trades_of(entry, exit_, N):
    out, pos, a = [], False, 0
    for i in range(N - 1):
        if not pos and entry[i]: pos, a = True, i + 1
        elif pos and exit_[i]: out.append((a, i + 1)); pos = False
    if pos: out.append((a, N - 1))
    return out

def run(sym, rules=None):   # rules(full_df) -> {name: (entry, exit)}: other rules on the same machinery (default: the six)
    rows = json.load(gzip.open(os.path.join(D, f"{sym}_4h.json.gz"), "rt"))
    d = pd.DataFrame(rows, columns="t o h l c v".split()); d["t"] = pd.to_datetime(d.t, unit="ms"); d = d.set_index("t")
    full = d[d.index < Z]   # indicators warm up on everything before the window; the window starts at A
    o, h, l, c = (full[k].values for k in "ohlc"); i0 = int(np.searchsorted(full.index.values, np.datetime64(A))); N = len(full)
    e50, e200 = ema(c, 50), ema(c, 200); r14, r2 = rsi(c, 14), rsi(c, 2); mac = ema(c, 12) - ema(c, 26); sig = ema(mac, 9)
    stu = supertrend_up(h, l, c); hh20 = pd.Series(h).rolling(20).max().shift(1).values; ll10 = pd.Series(l).rolling(10).min().shift(1).values
    s200, s5 = sma(c, 200), sma(c, 5); stu_prev = np.concatenate([[stu[0]], stu[:-1]])
    S = rules(full.reset_index(drop=True)) if rules else {"EMA 50/200 cross": (cross(e50, e200), cross(e200, e50)),
         "RSI under 30 / over 70": (r14 < 30, r14 > 70),
         "MACD cross above the 200 EMA": (cross(mac, sig) & (c > e200), cross(sig, mac)),
         "Supertrend 10/3": (stu & ~stu_prev, ~stu & stu_prev),
         "Turtle 20/10 breakout": (c > hh20, c < ll10),
         "RSI-2 (Connors)": ((c > s200) & (r2 < 10), c > s5)}
    lo = np.log(o); rng = np.random.default_rng(7); n = N - i0; out = {}
    bh = o[-1] / o[i0] * (1 - FEE) ** 2
    for name, (en, ex) in S.items():
        en = np.nan_to_num(en).astype(bool); ex = np.nan_to_num(ex).astype(bool); en[:i0] = False   # no entry before the window
        tr = trades_of(en, ex, N); k = len(tr); a = np.array([t[0] for t in tr]); b = np.array([t[1] for t in tr])
        r = o[b] / o[a] * (1 - FEE) ** 2; x = float(np.prod(r)); x0 = float(np.prod(o[b] / o[a])); dur = b - a
        G = (N - 1 - i0) - int(dur.sum())
        P = rng.permuted(np.tile(dur, (M, 1)), axis=1); cuts = np.sort(rng.integers(0, G + 1, (M, k)), axis=1)
        st = i0 + cuts + np.concatenate([np.zeros((M, 1), int), np.cumsum(P, axis=1)[:, :-1]], axis=1)
        mk = np.exp((lo[st + P] - lo[st]).sum(axis=1) + 2 * k * np.log(1 - FEE))
        w, ls = r[r > 1] - 1, r[r <= 1] - 1
        out[name] = dict(trades=k, win=float((r > 1).mean()), avg_win=float(w.mean()) if len(w) else 0.0, avg_loss=float(ls.mean()) if len(ls) else 0.0,
                         worst=float(r.min() - 1), x=x, x_nofee=x0, fees_cost=1 - x / x0, exposure=float(dur.sum() / n),
                         monkey_median=float(np.median(mk)), beats=float((mk < x).mean()),
                         first=str(full.index[i0].date()), last=str(full.index[-1]), _tr=(a, b), _mk=mk)
    return dict(bh=bh, bars=n, first=str(full.index[i0].date()), res=out, _o=o, _idx=full.index, _i0=i0)

if __name__ == "__main__":
    allr = {}
    for s in COINS:
        R = run(s); allr[s] = R
        print(f"\n{s} 4h {R['first']} -> {Z} bars={R['bars']} fee/side={FEE:.2%} buy&hold x{R['bh']:.2f}")
        for k, v in R["res"].items():
            print(f"  {k:30s} trades={v['trades']:4d} win={v['win']:4.0%} avgW={v['avg_win']:+6.1%} avgL={v['avg_loss']:+6.1%} worst={v['worst']:+6.1%} "
                  f"expo={v['exposure']:4.0%} x{v['x_nofee']:7.2f} nofee -> x{v['x']:7.2f} | monkey median x{v['monkey_median']:6.2f} | beats {v['beats']:4.0%}")
    clean = {s: dict({k: v for k, v in R.items() if not k.startswith('_')}, res={n: {k: v for k, v in r.items() if not k.startswith('_')} for n, r in R['res'].items()}) for s, R in allr.items()}
    json.dump(dict(fee=FEE, frm=A, to=Z, coins=clean), open(os.path.join(D, f"monkey_{FEE}_{A}_{Z}.json"), "w"), indent=1)
