File size: 8,593 Bytes
e99d55c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
"""quad_autopilot.py - paper autopilot for the PolyMarket crypto Up/Down quad.

Targets the 15-minute "Up or Down" markets for BTC / ETH / SOL / XRP.
Validated edge (fresh 1000-bar OOS, after 3c spread):
  RSI(14) < 30 -> BUY YES(up), resolved at close[t+3]: hit 0.788, ROI +0.258
  (5-min horizon had no edge: hit 0.515, ROI -0.015 -> skipped)

Per tick (every 5 min via cron):
  1. Binance 5m candles for each coin
  2. RSI(14) at last closed candle; RSI < 30 -> oversold candidate
  3. Find the live accepting 15-min Up/Down market for that coin (gamma-api)
  4. Optional Kronos veto (--kronos): skip if P(up) < 0.10
  5. Log paper BUY_YES + projected resolve to the ledger CSV; push ledger to HF bucket

PAPER ONLY: no real orders. Ledger rows are marked mode=paper.
"""
import argparse
import csv
import json
import os
import subprocess
import sys
import urllib.request
from datetime import datetime, timezone

COINS = ["BTC", "ETH", "SOL", "XRP"]
# Tradeable edge verified 2026-08-09 on 21 days x 4 coins (6048x5m bars):
# only ETH, 15-min products, RSI(14)<30, buy YES-up (+15m), breakeven 0.53.
# hit 0.609 (n=64), edge +0.079, walk-forward OOS 0.680. BTC/SOL/XRP = coin-flip.
# EDGE_COINS controls which coins may fire BUY_YES; "" = all (research mode).
EDGE_COINS = os.environ.get("EDGE_COINS", "ETH").split(",")
EDGE_THRESHOLD = float(os.environ.get("EDGE_THRESHOLD", "30"))
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"
GAMMA = "https://gamma-api.polymarket.com/markets?limit=100&closed=false&order=updatedAt&ascending=false&offset=0"
LEDGER = os.environ.get("LEDGER_PATH", "/home/neo/quad_ledger.csv")
HF_REPO = "40Hz/kronos-data"
HF_CMD = os.environ.get("HF_CMD", "hf")

FIELDS = ["ts", "mode", "coin", "market_id", "question", "rsi14", "side",
          "action", "yes_price", "resolve_ts", "kronos_prob_up", "kronos_veto", "note"]


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)
            opens = [float(x[1]) for x in arr]
            closes = [float(x[4]) for x in arr]
            ts = [int(x[0]) for x in arr]
            return opens, closes, ts
        except Exception as e:
            print(f"[{symbol}] {host} failed ({e}); trying next host")
            continue
    raise RuntimeError("all binance hosts failed")


def agg_bars(opens, closes, ts, bucket_min=15):
    """Aggregate 5m bars to bucket_min closes (floor to bucket; last close wins)."""
    buckets = {}
    for i in range(len(ts)):
        b = i // (bucket_min // 5)
        buckets[b] = closes[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 now_iso():
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")


def find_updown_markets():
    """Return list of (market) for 'Up or Down' markets still accepting, grouped by coin."""
    req = urllib.request.Request(GAMMA, headers={"User-Agent": "quad-lab/0.1"})
    with urllib.request.urlopen(req, timeout=60) as r:
        markets = json.load(r)
    out = []
    for m in markets:
        q = m.get("question") or ""
        if "up or down" not in q.lower():
            continue
        if not m.get("acceptingOrders"):
            continue
        out.append(m)
    return out


def pick_market(markets, coin, max_mins=20):
    """Choose the coin's 15-min Up/Down market that is still accepting and closes soonest."""
    now = datetime.now(timezone.utc)
    best = None
    for m in markets:
        q = m.get("question") or ""
        if not q.lower().startswith(coin + " up or down"):
            continue
        try:
            end = datetime.fromisoformat(m["endDate"].replace("Z", "+00:00"))
        except Exception:
            continue
        mins = (end - now).total_seconds() / 60
        if mins <= 0 or mins > max_mins:
            continue
        if best is None or mins < best[0]:
            best = (mins, m)
    return best


def kronos_veto(coin, prob_cache=None):
    """Optional GPU veto: skip if Kronos strongly DOWN (P(up) < 0.1).
    Returns (prob_up, veto). If unavailable -> (None, False)."""
    if prob_cache is None:
        return None, False
    prob = prob_cache.get(coin)
    if prob is None:
        return None, False
    return prob, prob < 0.10


def load_ledger():
    rows = []
    if os.path.exists(LEDGER):
        with open(LEDGER) as f:
            rd = csv.DictReader(f)
            rows = [r for r in rd]
    return rows


def append_ledger(row):
    exists = os.path.exists(LEDGER)
    with open(LEDGER, "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 push_ledger():
    try:
        subprocess.run([HF_CMD, "upload", HF_REPO, LEDGER, "--commit-message",
                        "quad autopilot ledger " + now_iso()], check=True,
                       capture_output=True, timeout=120)
        print("  ledger pushed to", HF_REPO)
    except Exception as e:
        print("  ledger push skipped:", e)


def main():
    ap = argparse.ArgumentParser()
    ap.add_argument("--kronos", action="store_true", help="enable Kronos veto (calls GPU Space)")
    ap.add_argument("--push", action="store_true", help="push ledger to HF bucket")
    ap.add_argument("--coin", choices=COINS + ["ALL"], default="ALL")
    args = ap.parse_args()

    coins = COINS if args.coin == "ALL" else [args.coin]
    markets = []
    if not args.kronos:
        try:
            markets = find_updown_markets()
        except Exception as e:
            print("market scan skipped:", e)

    prob_cache = {}  # filled only when --kronos implemented
    lines = []
    for coin in coins:
        try:
            opens, closes, ts = fetch_closes(coin)
        except Exception as e:
            print(f"[{coin}] fetch fail: {e}")
            continue
        rs5 = rsi(closes)
        rs15 = rsi(agg_bars(opens, closes, ts, 15))
        last_r5 = rs5[-1]
        last_r15 = rs15[-1]
        price = closes[-1]
        # Edge verified on 15-min RSI. Require the coin to be edge-enabled.
        edge_ok = coin in [c.strip().upper() for c in EDGE_COINS]
        trigger = last_r15 < EDGE_THRESHOLD and edge_ok
        prob, veto = kronos_veto(coin, prob_cache if args.kronos else None)

        market_id = question = yes_price = resolve_ts = ""
        if trigger and not veto:
            if markets:
                hit = pick_market(markets, coin)
                if hit:
                    mins, m = hit
                    market_id = m["id"]
                    question = m["question"]
                    yes_price = (m.get("outcomePrices") or "?")[:60]
                    resolve_ts = m["endDate"]
            action = "BUY_YES_15M_UP" if market_id else "BUY_YES_15M_UP_NO_MARKET"
        elif trigger and veto:
            action = "VETO"
        else:
            action = "NO_TRADE"

        note = (f"price={price:.2f} rsi15={last_r15:.1f} rsi5={last_r5:.1f} "
                f"trig={trigger} edge_ok={edge_ok} " +
                (f"kronos={prob:.3f} veto={veto} " if prob is not None else "") +
                ("market=" + market_id if market_id else ""))

        row = {"ts": now_iso(), "mode": "paper", "coin": coin, "market_id": market_id,
               "question": question, "rsi14": round(last_r15, 2), "side": "YES",
               "action": action, "yes_price": yes_price, "resolve_ts": resolve_ts,
               "kronos_prob_up": prob, "kronos_veto": bool(veto), "note": note}
        append_ledger(row)
        lines.append(f"[{coin}] rsi15={last_r15:.1f} rsi5={last_r5:.1f} "
                     f"price={price:.2f} -> {action} {note}")
        print(lines[-1])

    if args.push:
        push_ledger()
    print("done", now_iso())


if __name__ == "__main__":
    main()