Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
"""
|
| 2 |
-
RCEA Lite v1.0 — OKX Edition
|
| 3 |
-
Regime-Conditioned Evidence Accumulation
|
|
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
import math, time, warnings
|
|
@@ -31,6 +32,14 @@ FEAT_COLS = ["ret", "atr_norm", "ema_diff_norm", "efficiency", "rsi_dev", "dist_
|
|
| 31 |
TF_MAP = {"15m": "15m", "1h": "1H", "4h": "4H", "1d": "1D"}
|
| 32 |
TARGET_BARS = 1000
|
| 33 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
|
| 35 |
# ---------- Helpers ----------
|
| 36 |
def _f(x):
|
|
@@ -67,6 +76,99 @@ def _rsi(c, p=14):
|
|
| 67 |
return 100.0 - 100.0 / (1.0 + rs)
|
| 68 |
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
# ---------- OKX API ----------
|
| 71 |
def _req(path, params, timeout=TIMEOUT):
|
| 72 |
last = None
|
|
@@ -93,42 +195,29 @@ def _req(path, params, timeout=TIMEOUT):
|
|
| 93 |
|
| 94 |
|
| 95 |
def fetch_pairs(n=30):
|
| 96 |
-
"""Fetch USDT spot pairs ranked by 24h quote volume on OKX."""
|
| 97 |
inst = _req("/api/v5/public/instruments", {"instType": "SPOT"})
|
| 98 |
if not inst:
|
| 99 |
print("[RCEA] instruments unavailable", flush=True)
|
| 100 |
return []
|
| 101 |
-
|
| 102 |
usdt_syms = set()
|
| 103 |
for i in inst:
|
| 104 |
if i.get("state") != "live" or i.get("quoteCcy") != "USDT":
|
| 105 |
continue
|
| 106 |
sym, base = i.get("instId", ""), i.get("baseCcy", "").upper()
|
| 107 |
-
if not sym or not base:
|
| 108 |
-
|
| 109 |
-
if any(
|
| 110 |
-
continue
|
| 111 |
-
if any(sym.upper().endswith(p) for p in LEV_SUF):
|
| 112 |
-
continue
|
| 113 |
usdt_syms.add(sym)
|
| 114 |
-
|
| 115 |
if not usdt_syms:
|
| 116 |
-
print("[RCEA] no USDT spot pairs found", flush=True)
|
| 117 |
-
return []
|
| 118 |
-
|
| 119 |
tick = _req("/api/v5/market/tickers", {"instType": "SPOT"})
|
| 120 |
-
if not tick:
|
| 121 |
-
print("[RCEA] tickers unavailable", flush=True)
|
| 122 |
-
return []
|
| 123 |
-
|
| 124 |
cand = []
|
| 125 |
for t in tick:
|
| 126 |
sym = t.get("instId", "")
|
| 127 |
-
if sym not in usdt_syms:
|
| 128 |
-
continue
|
| 129 |
q = _f(t.get("volCcy24h"))
|
| 130 |
-
if q and q > 0:
|
| 131 |
-
cand.append((sym, q))
|
| 132 |
cand.sort(key=lambda x: x[1], reverse=True)
|
| 133 |
out = [c[0] for c in cand[:n]]
|
| 134 |
print(f"[RCEA] {len(out)} OKX USDT pairs resolved", flush=True)
|
|
@@ -136,55 +225,30 @@ def fetch_pairs(n=30):
|
|
| 136 |
|
| 137 |
|
| 138 |
def fetch_klines(inst_id, timeframe, target_bars=TARGET_BARS):
|
| 139 |
-
"""
|
| 140 |
-
Fetch OHLCV from OKX with pagination ('after' = oldest ts so far).
|
| 141 |
-
Excludes unconfirmed candles (confirm != '1').
|
| 142 |
-
"""
|
| 143 |
bar = TF_MAP[timeframe]
|
| 144 |
-
all_rows = []
|
| 145 |
-
after = None
|
| 146 |
-
max_pages = 10
|
| 147 |
-
|
| 148 |
for page in range(max_pages):
|
| 149 |
params = {"instId": inst_id, "bar": bar, "limit": "300"}
|
| 150 |
-
if after:
|
| 151 |
-
params["after"] = str(after)
|
| 152 |
raw = _req("/api/v5/market/candles", params)
|
| 153 |
-
if not raw:
|
| 154 |
-
break
|
| 155 |
-
|
| 156 |
got_new = 0
|
| 157 |
for k in raw:
|
| 158 |
-
if not isinstance(k, list) or len(k) < 9:
|
| 159 |
-
|
| 160 |
-
if k[8] != "1":
|
| 161 |
-
continue
|
| 162 |
o, h, l, c, v = _f(k[1]), _f(k[2]), _f(k[3]), _f(k[4]), _f(k[5])
|
| 163 |
-
if None in (o, h, l, c, v) or min(o, h, l, c) <= 0:
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
"open": o, "high": h, "low": l, "close": c, "volume": v})
|
| 167 |
got_new += 1
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
try:
|
| 173 |
-
after = int(raw[-1][0])
|
| 174 |
-
except (IndexError, ValueError):
|
| 175 |
-
break
|
| 176 |
-
|
| 177 |
-
if len(all_rows) >= target_bars:
|
| 178 |
-
break
|
| 179 |
time.sleep(0.12)
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
df = (pd.DataFrame(all_rows)
|
| 185 |
-
.drop_duplicates(subset=["ts"])
|
| 186 |
-
.sort_values("ts")
|
| 187 |
-
.reset_index(drop=True))
|
| 188 |
return df[["open", "high", "low", "close", "volume"]]
|
| 189 |
|
| 190 |
|
|
@@ -261,8 +325,13 @@ def forward_filter(m, f):
|
|
| 261 |
return post
|
| 262 |
|
| 263 |
|
| 264 |
-
# ---------- Evidence ----------
|
| 265 |
-
def evidence(f, rp, lam):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 266 |
n = len(f)
|
| 267 |
c = f["close"].values; ef = f["ema_fast"].values; es = f["ema_slow"].values
|
| 268 |
eff = f["efficiency"].values; mom = f["momentum"].values; am = f["atr_move"].values
|
|
@@ -279,54 +348,59 @@ def evidence(f, rp, lam):
|
|
| 279 |
em[i] = np.tanh(s * 2.0)
|
| 280 |
contr = max(0.0, 1.0 - vr[i])
|
| 281 |
ec[i] = np.tanh(bo[i] * (0.5 + 0.5 * contr) * 2.0)
|
| 282 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
acc = np.zeros(n); acc[0] = raw[0]
|
| 284 |
for i in range(1, n): acc[i] = lam * acc[i - 1] + (1 - lam) * raw[i]
|
| 285 |
pl = np.array([_sig(4.0 * x) for x in acc])
|
| 286 |
-
return acc, pl, 1.0 - pl
|
| 287 |
|
| 288 |
|
| 289 |
# ---------- Signal + Backtest ----------
|
| 290 |
-
def latest_signal(f, rp, acc, pl, ps, thr, mconf):
|
| 291 |
i = len(f) - 1
|
| 292 |
if i < MIN_SIG: return None
|
| 293 |
-
p =
|
|
|
|
| 294 |
if conf < mconf: return None
|
| 295 |
e, l, s = float(acc[i]), float(pl[i]), float(ps[i])
|
| 296 |
sig = "LONG" if e > thr and l > 0.5 else "SHORT" if e < -thr and s > 0.5 else "NO TRADE"
|
| 297 |
return {"signal": sig, "evidence": e, "P_long": l, "P_short": s,
|
| 298 |
"regime_probs": {REGIME_NAMES[j]: float(p[j]) for j in range(HMM_K)},
|
| 299 |
"dominant_regime": REGIME_NAMES[dom], "regime_conf": conf,
|
|
|
|
| 300 |
"close": float(f["close"].values[i])}
|
| 301 |
|
| 302 |
|
| 303 |
-
def backtest(sym, f, rp, acc, pl, ps, thr, mconf, maxhold):
|
| 304 |
n = len(f); c = f["close"].values; h = f["high"].values; lo = f["low"].values
|
| 305 |
atr = f["atr"].values
|
| 306 |
trades = []; in_pos = False; ei = eprice = d = sp = tp = 0
|
| 307 |
reg = ""; rconf = 0.0
|
| 308 |
|
| 309 |
-
# Backtest begins immediately after HMM training window + purge + embargo.
|
| 310 |
train_end = int(n * TRAIN_FRAC)
|
| 311 |
start = train_end + PURGE + EMBARGO
|
| 312 |
-
start = max(start, 30)
|
| 313 |
-
start = min(start, n - 2)
|
| 314 |
|
| 315 |
-
# DEBUG
|
| 316 |
if start < n:
|
| 317 |
pass_thr = int((np.abs(acc[start:]) > thr).sum())
|
| 318 |
-
pass_conf = int((
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
f"|acc|=[{np.abs(acc[start:]).min():.3f},{np.abs(acc[start:]).max():.3f}] "
|
| 323 |
-
f"thr_pass={pass_thr} conf_pass={pass_conf} both={both}",
|
| 324 |
flush=True)
|
| 325 |
|
| 326 |
for i in range(start, n):
|
| 327 |
if not in_pos:
|
| 328 |
-
p =
|
| 329 |
if conf < mconf: continue
|
|
|
|
|
|
|
| 330 |
ed = 1 if acc[i] > thr and pl[i] > 0.5 else -1 if acc[i] < -thr and ps[i] > 0.5 else 0
|
| 331 |
if ed == 0 or atr[i] < 1e-12: continue
|
| 332 |
eprice = c[i] * (1.0 + SLIPPAGE * ed); d = ed; ei = i; in_pos = True
|
|
@@ -449,25 +523,32 @@ def run(timeframe, n_coins, thr, lam, mconf, maxhold, progress=gr.Progress()):
|
|
| 449 |
print(f"[RCEA] {sym}: HMM fit failed (n={len(f)})", flush=True)
|
| 450 |
failed.append(sym); continue
|
| 451 |
rp = forward_filter(m, f)
|
| 452 |
-
|
| 453 |
-
|
|
|
|
|
|
|
|
|
|
| 454 |
if s: s["symbol"] = sym; signals.append(s)
|
| 455 |
-
sym_trades = backtest(sym, f, rp, acc, pl, ps, thr, mconf,
|
|
|
|
| 456 |
trades.extend(sym_trades)
|
| 457 |
-
print(f"[RCEA] {sym}: candles={len(f)}
|
| 458 |
-
f"
|
|
|
|
|
|
|
| 459 |
analyzed += 1
|
| 460 |
except Exception as e:
|
| 461 |
print(f"[RCEA] {sym}: EXC {e}", flush=True); failed.append(sym)
|
| 462 |
progress(1.0, desc="Done")
|
| 463 |
|
| 464 |
-
scan = ["# RCEA Lite — OKX
|
| 465 |
f"**Timeframe:** {timeframe} | **OKX bar:** {TF_MAP[timeframe]}",
|
| 466 |
f"**Coins requested:** {n_coins}",
|
| 467 |
f"**Analyzed:** {analyzed}",
|
| 468 |
f"**Failed:** {len(failed)}",
|
| 469 |
f"**Total trades:** {len(trades)}",
|
| 470 |
f"**Threshold:** {thr} | **λ:** {lam} | **MinConf:** {mconf} | **MaxHold:** {maxhold}",
|
|
|
|
| 471 |
f"**UTC:** {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')}"]
|
| 472 |
if len(trades) < 30:
|
| 473 |
scan.append(f"\n⚠️ Only {len(trades)} trades. Try threshold=0.04, "
|
|
@@ -480,6 +561,7 @@ def run(timeframe, n_coins, thr, lam, mconf, maxhold, progress=gr.Progress()):
|
|
| 480 |
"Evidence": round(s["evidence"], 4),
|
| 481 |
"P(Long)": round(s["P_long"], 4), "P(Short)": round(s["P_short"], 4),
|
| 482 |
"Regime": s["dominant_regime"], "Regime Conf": round(s["regime_conf"], 4),
|
|
|
|
| 483 |
"P(Trend)": round(s["regime_probs"]["TREND"], 4),
|
| 484 |
"P(MeanRev)": round(s["regime_probs"]["MEAN_REVERSION"], 4),
|
| 485 |
"P(Compress)": round(s["regime_probs"]["COMPRESSION"], 4),
|
|
@@ -488,8 +570,8 @@ def run(timeframe, n_coins, thr, lam, mconf, maxhold, progress=gr.Progress()):
|
|
| 488 |
sig_df = sig_df.sort_values(["_p", "Evidence"], ascending=[True, False]).drop(columns="_p")
|
| 489 |
else:
|
| 490 |
sig_df = pd.DataFrame(columns=["Symbol", "Signal", "Evidence", "P(Long)", "P(Short)",
|
| 491 |
-
"Regime", "Regime Conf", "
|
| 492 |
-
"P(Compress)", "Close"])
|
| 493 |
|
| 494 |
if trades:
|
| 495 |
tr_df = pd.DataFrame([{
|
|
@@ -527,9 +609,9 @@ def run(timeframe, n_coins, thr, lam, mconf, maxhold, progress=gr.Progress()):
|
|
| 527 |
|
| 528 |
|
| 529 |
# ---------- UI ----------
|
| 530 |
-
with gr.Blocks(title="RCEA Lite v1.0 — OKX") as demo:
|
| 531 |
-
gr.Markdown("# RCEA Lite v1.0 — OKX
|
| 532 |
-
"HMM regime detection
|
| 533 |
with gr.Row():
|
| 534 |
with gr.Column(scale=1):
|
| 535 |
tf = gr.Dropdown(["15m", "1h", "4h", "1d"], value="1h", label="Timeframe")
|
|
@@ -549,8 +631,9 @@ with gr.Blocks(title="RCEA Lite v1.0 — OKX") as demo:
|
|
| 549 |
with gr.Tab("🪙 Coins"): coin_tbl = gr.Dataframe(interactive=False)
|
| 550 |
btn.click(run, inputs=[tf, nc, thr_i, lam_i, mc_i, mh_i],
|
| 551 |
outputs=[scan_out, met_out, sig_tbl, tr_tbl, reg_tbl, coin_tbl])
|
| 552 |
-
gr.Markdown("---\n**
|
| 553 |
-
"
|
|
|
|
| 554 |
|
| 555 |
|
| 556 |
if __name__ == "__main__":
|
|
|
|
| 1 |
"""
|
| 2 |
+
RCEA Lite v1.0 — OKX Edition + BOCPD
|
| 3 |
+
Regime-Conditioned Evidence Accumulation with Bayesian Online Changepoint Detection.
|
| 4 |
+
Causal. No supervised ML.
|
| 5 |
"""
|
| 6 |
|
| 7 |
import math, time, warnings
|
|
|
|
| 32 |
TF_MAP = {"15m": "15m", "1h": "1H", "4h": "4H", "1d": "1D"}
|
| 33 |
TARGET_BARS = 1000
|
| 34 |
|
| 35 |
+
# BOCPD config
|
| 36 |
+
BOCPD_HAZARD = 1.0 / 250.0 # expected run length ~250 bars
|
| 37 |
+
BOCPD_MAX_RUN = 300 # truncate run-length distribution
|
| 38 |
+
BOCPD_PRIOR_MU = 0.0
|
| 39 |
+
BOCPD_PRIOR_KAPPA = 0.1
|
| 40 |
+
BOCPD_PRIOR_ALPHA = 0.1
|
| 41 |
+
BOCPD_PRIOR_BETA = 1e-4
|
| 42 |
+
|
| 43 |
|
| 44 |
# ---------- Helpers ----------
|
| 45 |
def _f(x):
|
|
|
|
| 76 |
return 100.0 - 100.0 / (1.0 + rs)
|
| 77 |
|
| 78 |
|
| 79 |
+
# ---------- BOCPD (Bayesian Online Changepoint Detection) ----------
|
| 80 |
+
def _student_t_pdf(x, mu, kappa, alpha, beta):
|
| 81 |
+
"""
|
| 82 |
+
Posterior predictive of Normal-Inverse-Gamma (Student-t).
|
| 83 |
+
Vectorised over arrays of (mu, kappa, alpha, beta) with scalar x.
|
| 84 |
+
"""
|
| 85 |
+
scale2 = beta * (kappa + 1.0) / (alpha * kappa)
|
| 86 |
+
scale2 = np.maximum(scale2, 1e-12)
|
| 87 |
+
df = 2.0 * alpha
|
| 88 |
+
z = (x - mu) / np.sqrt(scale2)
|
| 89 |
+
# log pdf of student-t
|
| 90 |
+
log_norm = (math.lgamma((df + 1.0) / 2.0)
|
| 91 |
+
- math.lgamma(df / 2.0)
|
| 92 |
+
- 0.5 * np.log(df * np.pi)
|
| 93 |
+
- 0.5 * np.log(scale2))
|
| 94 |
+
return np.exp(log_norm - 0.5 * (df + 1.0) * np.log1p(z * z / df))
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def bocpd_changepoint_prob(returns: np.ndarray,
|
| 98 |
+
hazard: float = BOCPD_HAZARD,
|
| 99 |
+
max_run: int = BOCPD_MAX_RUN) -> np.ndarray:
|
| 100 |
+
"""
|
| 101 |
+
Adams & MacKay (2007) BOCPD with Normal-Inverse-Gamma conjugate prior.
|
| 102 |
+
Returns per-bar changepoint probability P(r_t = 0 | x_1..t), causal.
|
| 103 |
+
"""
|
| 104 |
+
n = len(returns)
|
| 105 |
+
cp_prob = np.zeros(n)
|
| 106 |
+
|
| 107 |
+
R = np.array([1.0]) # run-length distribution, index = run length
|
| 108 |
+
mu_arr = np.array([BOCPD_PRIOR_MU])
|
| 109 |
+
kappa_arr = np.array([BOCPD_PRIOR_KAPPA])
|
| 110 |
+
alpha_arr = np.array([BOCPD_PRIOR_ALPHA])
|
| 111 |
+
beta_arr = np.array([BOCPD_PRIOR_BETA])
|
| 112 |
+
|
| 113 |
+
for t in range(n):
|
| 114 |
+
x = returns[t]
|
| 115 |
+
|
| 116 |
+
# 1. Predictive probability for each current run-length hypothesis
|
| 117 |
+
pred = _student_t_pdf(x, mu_arr, kappa_arr, alpha_arr, beta_arr)
|
| 118 |
+
|
| 119 |
+
# 2. Growth probabilities (run continues) + changepoint probability
|
| 120 |
+
growth = R * pred * (1.0 - hazard)
|
| 121 |
+
cp = np.sum(R * pred * hazard)
|
| 122 |
+
cp_prob[t] = cp / (cp + growth.sum() + 1e-300) if (cp + growth.sum()) > 1e-300 else 0.0
|
| 123 |
+
|
| 124 |
+
# 3. New run-length distribution
|
| 125 |
+
R_new = np.empty(len(R) + 1)
|
| 126 |
+
R_new[0] = cp
|
| 127 |
+
R_new[1:] = growth
|
| 128 |
+
# Renormalise
|
| 129 |
+
s = R_new.sum()
|
| 130 |
+
if s < 1e-300:
|
| 131 |
+
R_new = np.array([1.0])
|
| 132 |
+
else:
|
| 133 |
+
R_new = R_new / s
|
| 134 |
+
|
| 135 |
+
# 4. Update sufficient statistics (NIG conjugate update)
|
| 136 |
+
new_mu = np.empty(len(R_new))
|
| 137 |
+
new_kappa = np.empty(len(R_new))
|
| 138 |
+
new_alpha = np.empty(len(R_new))
|
| 139 |
+
new_beta = np.empty(len(R_new))
|
| 140 |
+
|
| 141 |
+
# New run (changepoint): reset to prior
|
| 142 |
+
new_mu[0] = BOCPD_PRIOR_MU
|
| 143 |
+
new_kappa[0] = BOCPD_PRIOR_KAPPA
|
| 144 |
+
new_alpha[0] = BOCPD_PRIOR_ALPHA
|
| 145 |
+
new_beta[0] = BOCPD_PRIOR_BETA
|
| 146 |
+
|
| 147 |
+
# Existing runs: NIG update
|
| 148 |
+
new_kappa[1:] = kappa_arr + 1.0
|
| 149 |
+
new_mu[1:] = (kappa_arr * mu_arr + x) / new_kappa[1:]
|
| 150 |
+
new_alpha[1:] = alpha_arr + 0.5
|
| 151 |
+
new_beta[1:] = (beta_arr
|
| 152 |
+
+ 0.5 * kappa_arr * (x - mu_arr) ** 2 / new_kappa[1:])
|
| 153 |
+
|
| 154 |
+
# 5. Truncate to max_run
|
| 155 |
+
if len(R_new) > max_run:
|
| 156 |
+
R_new = R_new[:max_run]
|
| 157 |
+
new_mu = new_mu[:max_run]
|
| 158 |
+
new_kappa = new_kappa[:max_run]
|
| 159 |
+
new_alpha = new_alpha[:max_run]
|
| 160 |
+
new_beta = new_beta[:max_run]
|
| 161 |
+
R_new = R_new / R_new.sum()
|
| 162 |
+
|
| 163 |
+
R = R_new
|
| 164 |
+
mu_arr = new_mu
|
| 165 |
+
kappa_arr = new_kappa
|
| 166 |
+
alpha_arr = new_alpha
|
| 167 |
+
beta_arr = new_beta
|
| 168 |
+
|
| 169 |
+
return cp_prob
|
| 170 |
+
|
| 171 |
+
|
| 172 |
# ---------- OKX API ----------
|
| 173 |
def _req(path, params, timeout=TIMEOUT):
|
| 174 |
last = None
|
|
|
|
| 195 |
|
| 196 |
|
| 197 |
def fetch_pairs(n=30):
|
|
|
|
| 198 |
inst = _req("/api/v5/public/instruments", {"instType": "SPOT"})
|
| 199 |
if not inst:
|
| 200 |
print("[RCEA] instruments unavailable", flush=True)
|
| 201 |
return []
|
|
|
|
| 202 |
usdt_syms = set()
|
| 203 |
for i in inst:
|
| 204 |
if i.get("state") != "live" or i.get("quoteCcy") != "USDT":
|
| 205 |
continue
|
| 206 |
sym, base = i.get("instId", ""), i.get("baseCcy", "").upper()
|
| 207 |
+
if not sym or not base: continue
|
| 208 |
+
if any(base.startswith(p) for p in LEV_PRE): continue
|
| 209 |
+
if any(sym.upper().endswith(p) for p in LEV_SUF): continue
|
|
|
|
|
|
|
|
|
|
| 210 |
usdt_syms.add(sym)
|
|
|
|
| 211 |
if not usdt_syms:
|
| 212 |
+
print("[RCEA] no USDT spot pairs found", flush=True); return []
|
|
|
|
|
|
|
| 213 |
tick = _req("/api/v5/market/tickers", {"instType": "SPOT"})
|
| 214 |
+
if not tick: return []
|
|
|
|
|
|
|
|
|
|
| 215 |
cand = []
|
| 216 |
for t in tick:
|
| 217 |
sym = t.get("instId", "")
|
| 218 |
+
if sym not in usdt_syms: continue
|
|
|
|
| 219 |
q = _f(t.get("volCcy24h"))
|
| 220 |
+
if q and q > 0: cand.append((sym, q))
|
|
|
|
| 221 |
cand.sort(key=lambda x: x[1], reverse=True)
|
| 222 |
out = [c[0] for c in cand[:n]]
|
| 223 |
print(f"[RCEA] {len(out)} OKX USDT pairs resolved", flush=True)
|
|
|
|
| 225 |
|
| 226 |
|
| 227 |
def fetch_klines(inst_id, timeframe, target_bars=TARGET_BARS):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
bar = TF_MAP[timeframe]
|
| 229 |
+
all_rows = []; after = None; max_pages = 10
|
|
|
|
|
|
|
|
|
|
| 230 |
for page in range(max_pages):
|
| 231 |
params = {"instId": inst_id, "bar": bar, "limit": "300"}
|
| 232 |
+
if after: params["after"] = str(after)
|
|
|
|
| 233 |
raw = _req("/api/v5/market/candles", params)
|
| 234 |
+
if not raw: break
|
|
|
|
|
|
|
| 235 |
got_new = 0
|
| 236 |
for k in raw:
|
| 237 |
+
if not isinstance(k, list) or len(k) < 9: continue
|
| 238 |
+
if k[8] != "1": continue
|
|
|
|
|
|
|
| 239 |
o, h, l, c, v = _f(k[1]), _f(k[2]), _f(k[3]), _f(k[4]), _f(k[5])
|
| 240 |
+
if None in (o, h, l, c, v) or min(o, h, l, c) <= 0: continue
|
| 241 |
+
all_rows.append({"ts": int(k[0]), "open": o, "high": h,
|
| 242 |
+
"low": l, "close": c, "volume": v})
|
|
|
|
| 243 |
got_new += 1
|
| 244 |
+
if got_new == 0: break
|
| 245 |
+
try: after = int(raw[-1][0])
|
| 246 |
+
except (IndexError, ValueError): break
|
| 247 |
+
if len(all_rows) >= target_bars: break
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
time.sleep(0.12)
|
| 249 |
+
if len(all_rows) < MIN_HMM: return None
|
| 250 |
+
df = (pd.DataFrame(all_rows).drop_duplicates(subset=["ts"])
|
| 251 |
+
.sort_values("ts").reset_index(drop=True))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
return df[["open", "high", "low", "close", "volume"]]
|
| 253 |
|
| 254 |
|
|
|
|
| 325 |
return post
|
| 326 |
|
| 327 |
|
| 328 |
+
# ---------- Evidence (with BOCPD blending) ----------
|
| 329 |
+
def evidence(f, rp, lam, cp_prob):
|
| 330 |
+
"""
|
| 331 |
+
Compute evidence with BOCPD-aware regime blending.
|
| 332 |
+
When changepoint probability is high, shrink regime posteriors toward uniform
|
| 333 |
+
-> evidence naturally dampens to neutral, avoiding stale-regime bets.
|
| 334 |
+
"""
|
| 335 |
n = len(f)
|
| 336 |
c = f["close"].values; ef = f["ema_fast"].values; es = f["ema_slow"].values
|
| 337 |
eff = f["efficiency"].values; mom = f["momentum"].values; am = f["atr_move"].values
|
|
|
|
| 348 |
em[i] = np.tanh(s * 2.0)
|
| 349 |
contr = max(0.0, 1.0 - vr[i])
|
| 350 |
ec[i] = np.tanh(bo[i] * (0.5 + 0.5 * contr) * 2.0)
|
| 351 |
+
|
| 352 |
+
# Blend posterior toward uniform proportional to changepoint probability
|
| 353 |
+
# If CP prob = 1, posteriors become uniform -> E_raw shrinks toward 0
|
| 354 |
+
rp_blended = rp * (1.0 - cp_prob[:, None]) + (1.0 / HMM_K) * cp_prob[:, None]
|
| 355 |
+
|
| 356 |
+
raw = rp_blended[:, 0] * et + rp_blended[:, 1] * em + rp_blended[:, 2] * ec
|
| 357 |
acc = np.zeros(n); acc[0] = raw[0]
|
| 358 |
for i in range(1, n): acc[i] = lam * acc[i - 1] + (1 - lam) * raw[i]
|
| 359 |
pl = np.array([_sig(4.0 * x) for x in acc])
|
| 360 |
+
return acc, pl, 1.0 - pl, rp_blended
|
| 361 |
|
| 362 |
|
| 363 |
# ---------- Signal + Backtest ----------
|
| 364 |
+
def latest_signal(f, rp, rp_blended, acc, pl, ps, thr, mconf, cp_prob):
|
| 365 |
i = len(f) - 1
|
| 366 |
if i < MIN_SIG: return None
|
| 367 |
+
p = rp_blended[i]; dom = int(np.argmax(p)); conf = float(p[dom])
|
| 368 |
+
# Do not trade when a changepoint is likely
|
| 369 |
if conf < mconf: return None
|
| 370 |
e, l, s = float(acc[i]), float(pl[i]), float(ps[i])
|
| 371 |
sig = "LONG" if e > thr and l > 0.5 else "SHORT" if e < -thr and s > 0.5 else "NO TRADE"
|
| 372 |
return {"signal": sig, "evidence": e, "P_long": l, "P_short": s,
|
| 373 |
"regime_probs": {REGIME_NAMES[j]: float(p[j]) for j in range(HMM_K)},
|
| 374 |
"dominant_regime": REGIME_NAMES[dom], "regime_conf": conf,
|
| 375 |
+
"changepoint": float(cp_prob[i]),
|
| 376 |
"close": float(f["close"].values[i])}
|
| 377 |
|
| 378 |
|
| 379 |
+
def backtest(sym, f, rp, rp_blended, acc, pl, ps, thr, mconf, maxhold, cp_prob):
|
| 380 |
n = len(f); c = f["close"].values; h = f["high"].values; lo = f["low"].values
|
| 381 |
atr = f["atr"].values
|
| 382 |
trades = []; in_pos = False; ei = eprice = d = sp = tp = 0
|
| 383 |
reg = ""; rconf = 0.0
|
| 384 |
|
|
|
|
| 385 |
train_end = int(n * TRAIN_FRAC)
|
| 386 |
start = train_end + PURGE + EMBARGO
|
| 387 |
+
start = max(start, 30); start = min(start, n - 2)
|
|
|
|
| 388 |
|
| 389 |
+
# DEBUG
|
| 390 |
if start < n:
|
| 391 |
pass_thr = int((np.abs(acc[start:]) > thr).sum())
|
| 392 |
+
pass_conf = int((rp_blended[start:].max(axis=1) >= mconf).sum())
|
| 393 |
+
pass_cp = int((cp_prob[start:] < 0.5).sum())
|
| 394 |
+
print(f"[BT] {sym}: n={n} window={n-start} "
|
| 395 |
+
f"thr_pass={pass_thr} conf_pass={pass_conf} cp_clear={pass_cp}",
|
|
|
|
|
|
|
| 396 |
flush=True)
|
| 397 |
|
| 398 |
for i in range(start, n):
|
| 399 |
if not in_pos:
|
| 400 |
+
p = rp_blended[i]; dom = int(np.argmax(p)); conf = float(p[dom])
|
| 401 |
if conf < mconf: continue
|
| 402 |
+
# Do not enter right at a changepoint
|
| 403 |
+
if cp_prob[i] > 0.5: continue
|
| 404 |
ed = 1 if acc[i] > thr and pl[i] > 0.5 else -1 if acc[i] < -thr and ps[i] > 0.5 else 0
|
| 405 |
if ed == 0 or atr[i] < 1e-12: continue
|
| 406 |
eprice = c[i] * (1.0 + SLIPPAGE * ed); d = ed; ei = i; in_pos = True
|
|
|
|
| 523 |
print(f"[RCEA] {sym}: HMM fit failed (n={len(f)})", flush=True)
|
| 524 |
failed.append(sym); continue
|
| 525 |
rp = forward_filter(m, f)
|
| 526 |
+
# BOCPD on log returns (causal)
|
| 527 |
+
rets = f["ret"].values
|
| 528 |
+
cp_prob = bocpd_changepoint_prob(rets)
|
| 529 |
+
acc, pl, ps, rp_blended = evidence(f, rp, lam, cp_prob)
|
| 530 |
+
s = latest_signal(f, rp, rp_blended, acc, pl, ps, thr, mconf, cp_prob)
|
| 531 |
if s: s["symbol"] = sym; signals.append(s)
|
| 532 |
+
sym_trades = backtest(sym, f, rp, rp_blended, acc, pl, ps, thr, mconf,
|
| 533 |
+
maxhold, cp_prob)
|
| 534 |
trades.extend(sym_trades)
|
| 535 |
+
print(f"[RCEA] {sym}: candles={len(f)} "
|
| 536 |
+
f"cp_max={cp_prob.max():.3f} cp_recent={cp_prob[-1]:.3f} "
|
| 537 |
+
f"signal={s['signal'] if s else 'NONE'} trades={len(sym_trades)}",
|
| 538 |
+
flush=True)
|
| 539 |
analyzed += 1
|
| 540 |
except Exception as e:
|
| 541 |
print(f"[RCEA] {sym}: EXC {e}", flush=True); failed.append(sym)
|
| 542 |
progress(1.0, desc="Done")
|
| 543 |
|
| 544 |
+
scan = ["# RCEA Lite — OKX + BOCPD",
|
| 545 |
f"**Timeframe:** {timeframe} | **OKX bar:** {TF_MAP[timeframe]}",
|
| 546 |
f"**Coins requested:** {n_coins}",
|
| 547 |
f"**Analyzed:** {analyzed}",
|
| 548 |
f"**Failed:** {len(failed)}",
|
| 549 |
f"**Total trades:** {len(trades)}",
|
| 550 |
f"**Threshold:** {thr} | **λ:** {lam} | **MinConf:** {mconf} | **MaxHold:** {maxhold}",
|
| 551 |
+
f"**BOCPD hazard:** {BOCPD_HAZARD:.4f} (~{int(1/BOCPD_HAZARD)} bars per regime)",
|
| 552 |
f"**UTC:** {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')}"]
|
| 553 |
if len(trades) < 30:
|
| 554 |
scan.append(f"\n⚠️ Only {len(trades)} trades. Try threshold=0.04, "
|
|
|
|
| 561 |
"Evidence": round(s["evidence"], 4),
|
| 562 |
"P(Long)": round(s["P_long"], 4), "P(Short)": round(s["P_short"], 4),
|
| 563 |
"Regime": s["dominant_regime"], "Regime Conf": round(s["regime_conf"], 4),
|
| 564 |
+
"Changepoint": round(s["changepoint"], 4),
|
| 565 |
"P(Trend)": round(s["regime_probs"]["TREND"], 4),
|
| 566 |
"P(MeanRev)": round(s["regime_probs"]["MEAN_REVERSION"], 4),
|
| 567 |
"P(Compress)": round(s["regime_probs"]["COMPRESSION"], 4),
|
|
|
|
| 570 |
sig_df = sig_df.sort_values(["_p", "Evidence"], ascending=[True, False]).drop(columns="_p")
|
| 571 |
else:
|
| 572 |
sig_df = pd.DataFrame(columns=["Symbol", "Signal", "Evidence", "P(Long)", "P(Short)",
|
| 573 |
+
"Regime", "Regime Conf", "Changepoint", "P(Trend)",
|
| 574 |
+
"P(MeanRev)", "P(Compress)", "Close"])
|
| 575 |
|
| 576 |
if trades:
|
| 577 |
tr_df = pd.DataFrame([{
|
|
|
|
| 609 |
|
| 610 |
|
| 611 |
# ---------- UI ----------
|
| 612 |
+
with gr.Blocks(title="RCEA Lite v1.0 — OKX + BOCPD") as demo:
|
| 613 |
+
gr.Markdown("# RCEA Lite v1.0 — OKX + BOCPD\n"
|
| 614 |
+
"HMM regime detection + Bayesian Online Changepoint Detection · Causal · No supervised ML")
|
| 615 |
with gr.Row():
|
| 616 |
with gr.Column(scale=1):
|
| 617 |
tf = gr.Dropdown(["15m", "1h", "4h", "1d"], value="1h", label="Timeframe")
|
|
|
|
| 631 |
with gr.Tab("🪙 Coins"): coin_tbl = gr.Dataframe(interactive=False)
|
| 632 |
btn.click(run, inputs=[tf, nc, thr_i, lam_i, mc_i, mh_i],
|
| 633 |
outputs=[scan_out, met_out, sig_tbl, tr_tbl, reg_tbl, coin_tbl])
|
| 634 |
+
gr.Markdown("---\n**BOCPD** (Adams & MacKay 2007): Bayesian changepoint probability per bar. "
|
| 635 |
+
"When CP > 0.5, new entries are blocked and posteriors shrink toward uniform. "
|
| 636 |
+
"**Data:** OKX. **Costs:** 0.10% + 0.05% per side.")
|
| 637 |
|
| 638 |
|
| 639 |
if __name__ == "__main__":
|