File size: 5,358 Bytes
d648a0d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
"""autoresearch.py - self-improvement loop for the quad autopilot.
Runs on GitHub Actions (hourly) / local cron. Cloud + local.

Tradeable grid = Polymarket 15-min Up/Down products only (verified live via
gamma-api: only 5-min and 15-min exist; no 1h). Signal = RSI(14) < threshold
on the 15-min close, buy YES-"up", win if close[t+1] > close[t] (+15m),
buy at 0.50 + 0.03 spread => breakeven hit 0.53.

21-day full-data study (2026-08-09, 6048x5m bars/coin) FALSIFIED the old
baseline (hit 0.788 / roi +0.258 - not reproducible on fresh data). Honest
result: only ETH clears the spread-adjusted breakeven (thr=30: hit 0.609,
n=64, edge +0.079, WF OOS 0.680). BTC/SOL/XRP are coin-flips. This script
tracks that grid and flags the signal that still works.
"""
import argparse
import csv
import json
import os
import urllib.request
from datetime import datetime, timezone

COINS = ["BTC", "ETH", "SOL", "XRP"]
BINANCE_HOSTS = os.environ.get("BINANCE_HOSTS", "data-api.binance.vision,api.binance.com").split(",")
BINANCE_TMPL = "https://{}/api/v3/klines?symbol={}USDT&interval=5m&limit=1200"
REPORT = os.environ.get("REPORT_PATH", "/home/neo/quad_research.csv")
OOS_BARS = int(os.environ.get("OOS_BARS", "200"))
THRESHOLDS = [int(x) for x in os.environ.get("THRESHOLDS", "20,25,30,35").split(",")]
BREAKEVEN = float(os.environ.get("BREAKEVEN", "0.53"))
EDGE_FLOOR = float(os.environ.get("EDGE_FLOOR", "0.03"))
MIN_N = int(os.environ.get("MIN_N", "10"))

FIELDS = ["ts", "coin", "bars", "oos_bars", "thr", "n", "hit", "edge",
          "breakeven", "status"]


def fetch_closes(symbol):
    for host in BINANCE_HOSTS:
        host = host.strip()
        url = BINANCE_TMPL.format(host, symbol)
        try:
            with urllib.request.urlopen(url, timeout=15) as r:
                arr = json.load(r)
            return [float(x[4]) for x in arr]
        except Exception as e:
            print(f"[{symbol}] {host} failed ({e}); trying next host")
            continue
    raise RuntimeError("all binance hosts failed")


def agg15(closes5):
    """Aggregate 5m closes to 15m closes (bucket = floor to 15m; last close wins)."""
    buckets = {}
    n = len(closes5)
    for i in range(n):
        buckets[i // 3] = closes5[i]
    return [buckets[k] for k in sorted(buckets)]


def rsi(closes, period=14):
    d = [closes[i] - closes[i - 1] for i in range(1, len(closes))]
    gains = [x if x > 0 else 0.0 for x in d]
    losses = [-x if x < 0 else 0.0 for x in d]
    ag = sum(gains[:period]) / period
    al = sum(losses[:period]) / period
    out = []
    for i in range(len(d)):
        if i >= period:
            ag = (ag * (period - 1) + gains[i]) / period
            al = (al * (period - 1) + losses[i]) / period
        out.append(100 if al == 0 else 100 - 100 / (1 + ag / al))
    return out


def validate(close15, thr, horizon=1, oos_bars=200):
    """OOS-only: last oos_bars 15m closes. Signal on candle t if RSI(t)<thr,
    win = close[t+1] > close[t] (+15m). edge = hit - breakeven."""
    n = len(close15)
    start = max(horizon + 1, n - oos_bars)
    rs = rsi(close15)
    rows = []
    for t in range(start, n - horizon):
        if rs[t - 1] < thr:
            rows.append(close15[t + horizon] > close15[t])
    if not rows:
        return 0, None
    hit = sum(rows) / len(rows)
    return len(rows), hit


def now_iso():
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def append_report(row):
    exists = os.path.exists(REPORT)
    d = os.path.dirname(REPORT)
    if d:
        os.makedirs(d, exist_ok=True)
    with open(REPORT, "a", newline="") as f:
        w = csv.DictWriter(f, fieldnames=FIELDS)
        if not exists:
            w.writeheader()
        w.writerow({k: row.get(k, "") for k in FIELDS})


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--coin", choices=COINS + ["ALL"], default="ALL")
    ap.add_argument("--thr", type=int, default=None)
    args = ap.parse_args()
    coins = COINS if args.coin == "ALL" else [args.coin]
    thrs = [args.thr] if args.thr else THRESHOLDS

    for coin in coins:
        try:
            closes5 = fetch_closes(coin)
        except Exception as e:
            print(f"[{coin}] fetch fail: {e}")
            continue
        c15 = agg15(closes5)
        rs = rsi(c15)
        current = round(rs[-1], 2) if rs[-1] is not None else None
        for thr in thrs:
            n, hit = validate(c15, thr, horizon=1, oos_bars=OOS_BARS)
            edge = round(hit - BREAKEVEN, 3) if hit is not None else None
            if n == 0 or hit is None:
                status = "NO_SIGNALS"
            elif edge is not None and edge >= EDGE_FLOOR and n >= MIN_N:
                status = "EDGE_OK"
            elif n < MIN_N:
                status = "LOW_N" if edge is not None else "NO_SIGNALS"
            else:
                status = "NO_EDGE"
            row = {"ts": now_iso(), "coin": coin, "bars": len(c15),
                   "oos_bars": OOS_BARS, "thr": thr, "n": n,
                   "hit": round(hit, 3) if hit is not None else "",
                   "edge": edge, "breakeven": BREAKEVEN, "status": status}
            append_report(row)
            print(f"[{coin}] thr={thr} n={n} hit={hit} edge={edge} "
                  f"(rsi_now={current}) -> {status}")
    print("done", now_iso())


if __name__ == "__main__":
    main()