Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,35 +1,40 @@
|
|
| 1 |
"""
|
| 2 |
-
RCEA Lite v1.0 —
|
| 3 |
-
Regime-Conditioned Evidence Accumulation
|
|
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
import math, time, warnings
|
| 7 |
from datetime import datetime, timezone
|
| 8 |
-
from typing import List, Optional
|
| 9 |
|
| 10 |
import numpy as np
|
| 11 |
import pandas as pd
|
| 12 |
-
import requests
|
| 13 |
import gradio as gr
|
| 14 |
from hmmlearn import hmm
|
| 15 |
|
| 16 |
warnings.filterwarnings("ignore")
|
| 17 |
|
| 18 |
# ---------- Config ----------
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
COST = 2 * (COMMISSION + SLIPPAGE)
|
| 23 |
REGIME_NAMES = ["TREND", "MEAN_REVERSION", "COMPRESSION"]
|
| 24 |
HMM_K = 3
|
| 25 |
TRAIN_FRAC, PURGE, EMBARGO = 0.60, 5, 3
|
| 26 |
MIN_HMM, MIN_SIG = 150, 80
|
| 27 |
-
LEV_PRE = ("3L", "3S", "5L", "5S", "BULL", "BEAR", "UP", "DOWN")
|
| 28 |
-
LEV_SUF = ("3LUSDT", "3SUSDT", "5LUSDT", "5SUSDT", "BULLUSDT", "BEARUSDT", "UPUSDT", "DOWNUSDT")
|
| 29 |
FEAT_COLS = ["ret", "atr_norm", "ema_diff_norm", "efficiency", "rsi_dev", "dist_ma"]
|
|
|
|
| 30 |
|
| 31 |
-
|
| 32 |
-
|
|
|
|
|
|
|
| 33 |
|
| 34 |
|
| 35 |
# ---------- Helpers ----------
|
|
@@ -67,135 +72,105 @@ def _rsi(c, p=14):
|
|
| 67 |
return 100.0 - 100.0 / (1.0 + rs)
|
| 68 |
|
| 69 |
|
| 70 |
-
# ----------
|
| 71 |
-
def
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
j = r.json()
|
| 79 |
-
if j.get("code") == "0":
|
| 80 |
-
return j.get("data")
|
| 81 |
-
last = f"code={j.get('code')} msg={j.get('msg', '')[:100]}"
|
| 82 |
-
elif r.status_code in (429, 418):
|
| 83 |
-
last = f"HTTP {r.status_code}"; time.sleep(5 * (attempt + 1))
|
| 84 |
-
elif r.status_code >= 500:
|
| 85 |
-
last = f"HTTP {r.status_code}"; time.sleep(DELAY * (attempt + 1))
|
| 86 |
-
else:
|
| 87 |
-
last = f"HTTP {r.status_code}: {r.text[:100]}"; break
|
| 88 |
-
except requests.RequestException as e:
|
| 89 |
-
last = f"{type(e).__name__}: {e}"
|
| 90 |
-
time.sleep(DELAY * (attempt + 1))
|
| 91 |
-
print(f"[RCEA] {path} failed: {last}", flush=True)
|
| 92 |
-
return None
|
| 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 |
-
continue
|
| 109 |
-
if any(base.startswith(p) for p in LEV_PRE):
|
| 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)
|
| 135 |
-
return out
|
| 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 |
-
continue
|
| 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 |
-
continue
|
| 165 |
-
all_rows.append({"ts": int(k[0]),
|
| 166 |
-
"open": o, "high": h, "low": l, "close": c, "volume": v})
|
| 167 |
-
got_new += 1
|
| 168 |
-
|
| 169 |
-
if got_new == 0:
|
| 170 |
-
break
|
| 171 |
|
| 172 |
-
try:
|
| 173 |
-
after = int(raw[-1][0])
|
| 174 |
-
except (IndexError, ValueError):
|
| 175 |
-
break
|
| 176 |
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
|
|
|
|
|
|
| 180 |
|
| 181 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 183 |
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
return df[["open", "high", "low", "close", "volume"]]
|
| 189 |
|
| 190 |
|
| 191 |
# ---------- Features ----------
|
| 192 |
def features(df):
|
| 193 |
-
c = df["close"].values.astype(float)
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
n = len(c)
|
| 197 |
-
o = pd.DataFrame(index=df.index)
|
| 198 |
-
o["close"] = c
|
| 199 |
r = np.zeros(n); r[1:] = np.diff(c) / c[:-1]; o["ret"] = r
|
| 200 |
a14 = _atr(h, lo, c, 14); o["atr"] = a14
|
| 201 |
o["atr_norm"] = a14 / np.where(c > 0, c, 1.0)
|
|
@@ -217,8 +192,7 @@ def features(df):
|
|
| 217 |
ae = np.where(a14 > 1e-12, (c - rm) / a14, 0.0); o["atr_extension"] = np.nan_to_num(ae)
|
| 218 |
a5, a20 = _atr(h, lo, c, 5), _atr(h, lo, c, 20)
|
| 219 |
o["vol_ratio"] = np.where(a20 > 1e-12, a5 / a20, 1.0)
|
| 220 |
-
ph, pl_ = np.roll(h, 1), np.roll(lo, 1)
|
| 221 |
-
ph[0], pl_[0] = h[0], lo[0]
|
| 222 |
o["prev_high"], o["prev_low"] = ph, pl_
|
| 223 |
o["breakout"] = np.where(c > ph, 1.0, np.where(c < pl_, -1.0, 0.0))
|
| 224 |
return o.replace([np.inf, -np.inf], np.nan).fillna(0.0)
|
|
@@ -239,8 +213,7 @@ def fit_hmm(f):
|
|
| 239 |
|
| 240 |
|
| 241 |
def forward_filter(m, f):
|
| 242 |
-
n = len(f)
|
| 243 |
-
post = np.ones((n, HMM_K)) / HMM_K
|
| 244 |
ntr = min(max(MIN_HMM, int(n * TRAIN_FRAC)), n - 20)
|
| 245 |
if ntr < 10: return post
|
| 246 |
X = (f[FEAT_COLS].values - m._mu) / m._sd
|
|
@@ -261,10 +234,9 @@ 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
|
| 269 |
rd = f["rsi_dev"].values; dm = f["dist_ma"].values; ae = f["atr_extension"].values
|
| 270 |
vr = f["vol_ratio"].values; bo = f["breakout"].values
|
|
@@ -279,54 +251,39 @@ 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,
|
| 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,
|
| 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) # need indicator warm-up
|
| 313 |
-
start = min(start, n - 2)
|
| 314 |
-
|
| 315 |
-
# DEBUG metrics
|
| 316 |
-
if start < n:
|
| 317 |
-
pass_thr = int((np.abs(acc[start:]) > thr).sum())
|
| 318 |
-
pass_conf = int((rp[start:].max(axis=1) >= mconf).sum())
|
| 319 |
-
both = int(((np.abs(acc[start:]) > thr) &
|
| 320 |
-
(rp[start:].max(axis=1) >= mconf)).sum())
|
| 321 |
-
print(f"[BT] {sym}: n={n} start={start} window={n-start} "
|
| 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
|
|
@@ -365,13 +322,12 @@ def metrics(trades):
|
|
| 365 |
r = np.array([t["net_return"] for t in trades])
|
| 366 |
w, l = r[r > 0], r[r <= 0]
|
| 367 |
gp = w.sum() if len(w) else 0.0; gl = abs(l.sum()) if len(l) else 0.0
|
| 368 |
-
eq = np.cumprod(1 + r); peak = np.maximum.accumulate(eq)
|
| 369 |
-
dd = (eq - peak) / peak
|
| 370 |
longs = [t for t in trades if t["direction"] == "LONG"]
|
| 371 |
shorts = [t for t in trades if t["direction"] == "SHORT"]
|
| 372 |
lw = sum(1 for t in longs if t["win"]); sw = sum(1 for t in shorts if t["win"])
|
| 373 |
-
return dict(total_trades=len(r),
|
| 374 |
-
|
| 375 |
net_expectancy=float(r.mean()), avg_win=float(w.mean()) if len(w) else 0.0,
|
| 376 |
avg_loss=float(l.mean()) if len(l) else 0.0,
|
| 377 |
max_drawdown=float(dd.min()) if len(dd) else 0.0,
|
|
@@ -388,11 +344,9 @@ def regime_table(trades):
|
|
| 388 |
rt = [t for t in trades if t["regime"] == reg]
|
| 389 |
if not rt:
|
| 390 |
rows.append({"Regime": reg, "Trades": 0, "WinRate": 0.0, "PF": 0.0, "NetExp": 0.0}); continue
|
| 391 |
-
n = np.array([t["net_return"] for t in rt])
|
| 392 |
-
w, l = n[n > 0], n[n <= 0]
|
| 393 |
gp = w.sum() if len(w) else 0.0; gl = abs(l.sum()) if len(l) else 0.0
|
| 394 |
-
rows.append({"Regime": reg, "Trades": len(rt),
|
| 395 |
-
"WinRate": round(len(w) / len(rt), 4),
|
| 396 |
"PF": round(gp / gl, 4) if gl > 1e-12 else 999.0,
|
| 397 |
"NetExp": round(float(n.mean()), 6)})
|
| 398 |
return pd.DataFrame(rows)
|
|
@@ -403,83 +357,67 @@ def coin_table(trades):
|
|
| 403 |
rows = []
|
| 404 |
for sym in sorted({t["symbol"] for t in trades}):
|
| 405 |
st = [t for t in trades if t["symbol"] == sym]
|
| 406 |
-
n = np.array([t["net_return"] for t in st])
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
"WinRate": round(len(w) / len(st), 4),
|
| 410 |
-
"NetExp": round(float(n.mean()), 6),
|
| 411 |
-
"TotalNet": round(float(n.sum()), 6)})
|
| 412 |
return pd.DataFrame(rows).sort_values("TotalNet", ascending=False)
|
| 413 |
|
| 414 |
|
| 415 |
# ---------- Pipeline ----------
|
| 416 |
-
def run(timeframe,
|
| 417 |
def err(msg, detail=""):
|
| 418 |
return msg, detail, pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), pd.DataFrame()
|
| 419 |
|
| 420 |
-
|
| 421 |
-
thr = max(0.0, min(1.0, float(thr)))
|
| 422 |
-
|
| 423 |
-
mconf = max(0.0, min(1.0, float(mconf)))
|
| 424 |
-
maxhold = max(5, min(200, int(maxhold)))
|
| 425 |
-
|
| 426 |
if timeframe not in TF_MAP:
|
| 427 |
return err("ERROR: Invalid timeframe")
|
| 428 |
|
| 429 |
-
progress(0.02, desc="
|
| 430 |
-
|
| 431 |
-
except Exception as e: return err(f"ERROR fetching pairs: {e}")
|
| 432 |
-
if not pairs:
|
| 433 |
-
return err("ERROR: No OKX USDT pairs found.\n\n"
|
| 434 |
-
"Possible causes: OKX API blocked from this region, rate limit, or timeout. "
|
| 435 |
-
"Check Space logs for `[RCEA] ...` messages.")
|
| 436 |
|
| 437 |
signals, trades, analyzed, failed = [], [], 0, []
|
| 438 |
for i, sym in enumerate(pairs):
|
| 439 |
progress((i + 1) / len(pairs) * 0.9, desc=f"{sym} ({i+1}/{len(pairs)})")
|
| 440 |
-
time.sleep(0.15)
|
| 441 |
df = fetch_klines(sym, timeframe)
|
| 442 |
if df is None:
|
| 443 |
-
print(f"[RCEA] {sym}: insufficient candles", flush=True)
|
| 444 |
failed.append(sym); continue
|
| 445 |
try:
|
| 446 |
-
f = features(df)
|
| 447 |
-
m
|
| 448 |
-
if m is None:
|
| 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,
|
| 456 |
trades.extend(sym_trades)
|
| 457 |
-
print(f"[RCEA] {sym}: candles={len(f)}
|
| 458 |
-
f"trades={len(sym_trades)}", flush=True)
|
| 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 —
|
| 465 |
-
f"**Timeframe:** {timeframe}
|
| 466 |
-
f"**
|
| 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, "
|
| 474 |
-
f"λ=0.65, MinConf=0.25, or Coins=100.")
|
| 475 |
scan_report = "\n".join(scan)
|
| 476 |
|
| 477 |
if signals:
|
| 478 |
sig_df = pd.DataFrame([{
|
| 479 |
"Symbol": s["symbol"], "Signal": s["signal"],
|
| 480 |
-
"Evidence": round(s["evidence"], 4),
|
| 481 |
-
"P(
|
| 482 |
-
"Regime": s["
|
| 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,20 +426,19 @@ 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([{
|
| 496 |
-
"Symbol": t["symbol"], "Direction": t["direction"],
|
| 497 |
-
"
|
| 498 |
-
"Bars": t["bars_held"],
|
| 499 |
"Entry": round(t["entry_price"], 6), "Exit": round(t["exit_price"], 6),
|
| 500 |
"Stop": round(t["stop_price"], 6), "Target": round(t["target_price"], 6),
|
| 501 |
-
"Reason": t["exit_reason"],
|
| 502 |
-
"
|
| 503 |
-
"Regime": t["
|
| 504 |
-
|
| 505 |
else:
|
| 506 |
tr_df = pd.DataFrame(columns=["Symbol", "Direction", "Entry Bar", "Exit Bar", "Bars",
|
| 507 |
"Entry", "Exit", "Stop", "Target", "Reason",
|
|
@@ -510,13 +447,10 @@ def run(timeframe, n_coins, thr, lam, mconf, maxhold, progress=gr.Progress()):
|
|
| 510 |
m = metrics(trades)
|
| 511 |
m_str = "\n".join([
|
| 512 |
"# Performance Metrics (net of costs)",
|
| 513 |
-
f"**Total trades:** {m['total_trades']}",
|
| 514 |
-
f"**
|
| 515 |
-
f"**Profit factor:** {m['profit_factor']:.3f}",
|
| 516 |
-
f"**Net expectancy:** {m['net_expectancy']:.6f}",
|
| 517 |
f"**Avg win / loss:** {m['avg_win']:.6f} / {m['avg_loss']:.6f}",
|
| 518 |
-
f"**Max DD:** {m['max_drawdown']:.2%}",
|
| 519 |
-
f"**Net compounded:** {m['net_compounded_return']:.2%}",
|
| 520 |
f"**Long:** {m['long_trades']} ({m['long_win_rate']:.2%}) | "
|
| 521 |
f"**Short:** {m['short_trades']} ({m['short_win_rate']:.2%})",
|
| 522 |
])
|
|
@@ -527,13 +461,13 @@ 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 —
|
| 531 |
-
gr.Markdown("# RCEA Lite v1.0 —
|
| 532 |
-
"HMM
|
| 533 |
with gr.Row():
|
| 534 |
with gr.Column(scale=1):
|
| 535 |
tf = gr.Dropdown(["15m", "1h", "4h", "1d"], value="1h", label="Timeframe")
|
| 536 |
-
nc = gr.Slider(
|
| 537 |
thr_i = gr.Slider(0.0, 1.0, value=0.06, step=0.01, label="Evidence threshold")
|
| 538 |
lam_i = gr.Slider(0.0, 0.99, value=0.70, step=0.01, label="Memory λ")
|
| 539 |
mc_i = gr.Slider(0.0, 1.0, value=0.30, step=0.01, label="Min regime confidence")
|
|
@@ -546,11 +480,11 @@ with gr.Blocks(title="RCEA Lite v1.0 — OKX") as demo:
|
|
| 546 |
with gr.Tab("📡 Signals"): sig_tbl = gr.Dataframe(interactive=False, wrap=True)
|
| 547 |
with gr.Tab("📜 Trades"): tr_tbl = gr.Dataframe(interactive=False, wrap=True)
|
| 548 |
with gr.Tab("📊 Regime"): reg_tbl = gr.Dataframe(interactive=False)
|
| 549 |
-
with gr.Tab("🪙
|
| 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**Data
|
| 553 |
-
"Net of 0.
|
| 554 |
|
| 555 |
|
| 556 |
if __name__ == "__main__":
|
|
|
|
| 1 |
"""
|
| 2 |
+
RCEA Lite v1.0 — Forex Edition (GitHub CSV)
|
| 3 |
+
Regime-Conditioned Evidence Accumulation + BOCPD.
|
| 4 |
+
Causal. No supervised ML. No API key required.
|
| 5 |
"""
|
| 6 |
|
| 7 |
import math, time, warnings
|
| 8 |
from datetime import datetime, timezone
|
|
|
|
| 9 |
|
| 10 |
import numpy as np
|
| 11 |
import pandas as pd
|
|
|
|
| 12 |
import gradio as gr
|
| 13 |
from hmmlearn import hmm
|
| 14 |
|
| 15 |
warnings.filterwarnings("ignore")
|
| 16 |
|
| 17 |
# ---------- Config ----------
|
| 18 |
+
GITHUB_RAW = "https://raw.githubusercontent.com/ejtraderLabs/historical-data/main"
|
| 19 |
+
FOREX_SYMBOLS = [
|
| 20 |
+
"EURUSD", "GBPUSD", "USDJPY", "AUDUSD", "USDCAD",
|
| 21 |
+
"USDCHF", "EURGBP", "EURJPY", "GBPJPY", "EURCHF",
|
| 22 |
+
"AUDJPY", "XAUUSD",
|
| 23 |
+
]
|
| 24 |
+
TF_MAP = {"15m": "m15", "1h": "h1", "4h": "h4", "1d": "d1"}
|
| 25 |
+
COMMISSION, SLIPPAGE = 0.0002, 0.0001
|
| 26 |
COST = 2 * (COMMISSION + SLIPPAGE)
|
| 27 |
REGIME_NAMES = ["TREND", "MEAN_REVERSION", "COMPRESSION"]
|
| 28 |
HMM_K = 3
|
| 29 |
TRAIN_FRAC, PURGE, EMBARGO = 0.60, 5, 3
|
| 30 |
MIN_HMM, MIN_SIG = 150, 80
|
|
|
|
|
|
|
| 31 |
FEAT_COLS = ["ret", "atr_norm", "ema_diff_norm", "efficiency", "rsi_dev", "dist_ma"]
|
| 32 |
+
TARGET_BARS = 2000
|
| 33 |
|
| 34 |
+
BOCPD_HAZARD = 1.0 / 250.0
|
| 35 |
+
BOCPD_MAX_RUN = 300
|
| 36 |
+
BOCPD_PRIOR_MU, BOCPD_PRIOR_KAPPA = 0.0, 0.1
|
| 37 |
+
BOCPD_PRIOR_ALPHA, BOCPD_PRIOR_BETA = 0.1, 1e-4
|
| 38 |
|
| 39 |
|
| 40 |
# ---------- Helpers ----------
|
|
|
|
| 72 |
return 100.0 - 100.0 / (1.0 + rs)
|
| 73 |
|
| 74 |
|
| 75 |
+
# ---------- BOCPD ----------
|
| 76 |
+
def _student_t_pdf(x, mu, kappa, alpha, beta):
|
| 77 |
+
scale2 = np.maximum(beta * (kappa + 1.0) / (alpha * kappa), 1e-12)
|
| 78 |
+
df = 2.0 * alpha
|
| 79 |
+
z = (x - mu) / np.sqrt(scale2)
|
| 80 |
+
log_norm = (math.lgamma((df + 1.0) / 2.0) - math.lgamma(df / 2.0)
|
| 81 |
+
- 0.5 * np.log(df * np.pi) - 0.5 * np.log(scale2))
|
| 82 |
+
return np.exp(log_norm - 0.5 * (df + 1.0) * np.log1p(z * z / df))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
+
def bocpd_changepoint_prob(returns, hazard=BOCPD_HAZARD, max_run=BOCPD_MAX_RUN):
|
| 86 |
+
n = len(returns); cp_prob = np.zeros(n)
|
| 87 |
+
R = np.array([1.0])
|
| 88 |
+
mu_arr = np.array([BOCPD_PRIOR_MU]); kappa_arr = np.array([BOCPD_PRIOR_KAPPA])
|
| 89 |
+
alpha_arr = np.array([BOCPD_PRIOR_ALPHA]); beta_arr = np.array([BOCPD_PRIOR_BETA])
|
| 90 |
|
| 91 |
+
for t in range(n):
|
| 92 |
+
x = returns[t]
|
| 93 |
+
pred = _student_t_pdf(x, mu_arr, kappa_arr, alpha_arr, beta_arr)
|
| 94 |
+
growth = R * pred * (1.0 - hazard)
|
| 95 |
+
cp = np.sum(R * pred * hazard)
|
| 96 |
+
denom = cp + growth.sum()
|
| 97 |
+
cp_prob[t] = cp / denom if denom > 1e-300 else 0.0
|
| 98 |
+
|
| 99 |
+
R_new = np.empty(len(R) + 1); R_new[0] = cp; R_new[1:] = growth
|
| 100 |
+
s = R_new.sum(); R_new = R_new / s if s > 1e-300 else np.array([1.0])
|
| 101 |
+
|
| 102 |
+
new_mu = np.empty(len(R_new)); new_kappa = np.empty(len(R_new))
|
| 103 |
+
new_alpha = np.empty(len(R_new)); new_beta = np.empty(len(R_new))
|
| 104 |
+
new_mu[0] = BOCPD_PRIOR_MU; new_kappa[0] = BOCPD_PRIOR_KAPPA
|
| 105 |
+
new_alpha[0] = BOCPD_PRIOR_ALPHA; new_beta[0] = BOCPD_PRIOR_BETA
|
| 106 |
+
new_kappa[1:] = kappa_arr + 1.0
|
| 107 |
+
new_mu[1:] = (kappa_arr * mu_arr + x) / new_kappa[1:]
|
| 108 |
+
new_alpha[1:] = alpha_arr + 0.5
|
| 109 |
+
new_beta[1:] = beta_arr + 0.5 * kappa_arr * (x - mu_arr) ** 2 / new_kappa[1:]
|
| 110 |
+
|
| 111 |
+
if len(R_new) > max_run:
|
| 112 |
+
R_new = R_new[:max_run]; new_mu = new_mu[:max_run]
|
| 113 |
+
new_kappa = new_kappa[:max_run]; new_alpha = new_alpha[:max_run]
|
| 114 |
+
new_beta = new_beta[:max_run]; R_new = R_new / R_new.sum()
|
| 115 |
+
|
| 116 |
+
R = R_new; mu_arr = new_mu; kappa_arr = new_kappa
|
| 117 |
+
alpha_arr = new_alpha; beta_arr = new_beta
|
| 118 |
+
return cp_prob
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
# ---------- Data (GitHub CSV) ----------
|
| 122 |
+
def fetch_pairs(n, api_key=None):
|
| 123 |
+
pairs = FOREX_SYMBOLS[:max(1, min(n, len(FOREX_SYMBOLS)))]
|
| 124 |
+
print(f"[RCEA] {len(pairs)} forex symbols selected", flush=True)
|
| 125 |
+
return pairs
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def fetch_klines(symbol, timeframe, api_key=None, target_bars=TARGET_BARS):
|
| 129 |
+
tf = TF_MAP.get(timeframe)
|
| 130 |
+
if tf is None:
|
| 131 |
return None
|
| 132 |
+
symbol = symbol.upper()
|
| 133 |
+
url = f"{GITHUB_RAW}/{symbol}/{symbol}{tf}.csv"
|
| 134 |
+
try:
|
| 135 |
+
df = pd.read_csv(url)
|
| 136 |
+
except Exception as e:
|
| 137 |
+
print(f"[RCEA] {symbol} CSV fetch failed: {e}", flush=True)
|
| 138 |
+
return None
|
| 139 |
+
|
| 140 |
+
df.columns = [c.strip().lower() for c in df.columns]
|
| 141 |
+
if "date" in df.columns:
|
| 142 |
+
df = df.rename(columns={"date": "datetime"})
|
| 143 |
+
elif "time" in df.columns:
|
| 144 |
+
df = df.rename(columns={"time": "datetime"})
|
| 145 |
|
| 146 |
+
required = {"open", "high", "low", "close"}
|
| 147 |
+
if not required.issubset(df.columns):
|
| 148 |
+
print(f"[RCEA] {symbol}: missing columns", flush=True)
|
| 149 |
+
return None
|
| 150 |
+
|
| 151 |
+
for col in ["open", "high", "low", "close"]:
|
| 152 |
+
df[col] = pd.to_numeric(df[col], errors="coerce")
|
| 153 |
+
if "tick_volume" in df.columns:
|
| 154 |
+
df["volume"] = pd.to_numeric(df["tick_volume"], errors="coerce").fillna(0.0)
|
| 155 |
+
elif "volume" in df.columns:
|
| 156 |
+
df["volume"] = pd.to_numeric(df["volume"], errors="coerce").fillna(0.0)
|
| 157 |
+
else:
|
| 158 |
+
df["volume"] = 0.0
|
| 159 |
+
|
| 160 |
+
df = df.dropna(subset=["open", "high", "low", "close"]).reset_index(drop=True)
|
| 161 |
+
if len(df) > target_bars:
|
| 162 |
+
df = df.iloc[-target_bars:].reset_index(drop=True)
|
| 163 |
+
if len(df) < MIN_HMM:
|
| 164 |
+
print(f"[RCEA] {symbol}: insufficient rows ({len(df)})", flush=True)
|
| 165 |
+
return None
|
| 166 |
return df[["open", "high", "low", "close", "volume"]]
|
| 167 |
|
| 168 |
|
| 169 |
# ---------- Features ----------
|
| 170 |
def features(df):
|
| 171 |
+
c = df["close"].values.astype(float); h = df["high"].values.astype(float)
|
| 172 |
+
lo = df["low"].values.astype(float); n = len(c)
|
| 173 |
+
o = pd.DataFrame(index=df.index); o["close"] = c
|
|
|
|
|
|
|
|
|
|
| 174 |
r = np.zeros(n); r[1:] = np.diff(c) / c[:-1]; o["ret"] = r
|
| 175 |
a14 = _atr(h, lo, c, 14); o["atr"] = a14
|
| 176 |
o["atr_norm"] = a14 / np.where(c > 0, c, 1.0)
|
|
|
|
| 192 |
ae = np.where(a14 > 1e-12, (c - rm) / a14, 0.0); o["atr_extension"] = np.nan_to_num(ae)
|
| 193 |
a5, a20 = _atr(h, lo, c, 5), _atr(h, lo, c, 20)
|
| 194 |
o["vol_ratio"] = np.where(a20 > 1e-12, a5 / a20, 1.0)
|
| 195 |
+
ph, pl_ = np.roll(h, 1), np.roll(lo, 1); ph[0], pl_[0] = h[0], lo[0]
|
|
|
|
| 196 |
o["prev_high"], o["prev_low"] = ph, pl_
|
| 197 |
o["breakout"] = np.where(c > ph, 1.0, np.where(c < pl_, -1.0, 0.0))
|
| 198 |
return o.replace([np.inf, -np.inf], np.nan).fillna(0.0)
|
|
|
|
| 213 |
|
| 214 |
|
| 215 |
def forward_filter(m, f):
|
| 216 |
+
n = len(f); post = np.ones((n, HMM_K)) / HMM_K
|
|
|
|
| 217 |
ntr = min(max(MIN_HMM, int(n * TRAIN_FRAC)), n - 20)
|
| 218 |
if ntr < 10: return post
|
| 219 |
X = (f[FEAT_COLS].values - m._mu) / m._sd
|
|
|
|
| 234 |
return post
|
| 235 |
|
| 236 |
|
| 237 |
+
# ---------- Evidence + BOCPD blend ----------
|
| 238 |
+
def evidence(f, rp, lam, cp_prob):
|
| 239 |
+
n = len(f); c = f["close"].values; ef = f["ema_fast"].values; es = f["ema_slow"].values
|
|
|
|
| 240 |
eff = f["efficiency"].values; mom = f["momentum"].values; am = f["atr_move"].values
|
| 241 |
rd = f["rsi_dev"].values; dm = f["dist_ma"].values; ae = f["atr_extension"].values
|
| 242 |
vr = f["vol_ratio"].values; bo = f["breakout"].values
|
|
|
|
| 251 |
em[i] = np.tanh(s * 2.0)
|
| 252 |
contr = max(0.0, 1.0 - vr[i])
|
| 253 |
ec[i] = np.tanh(bo[i] * (0.5 + 0.5 * contr) * 2.0)
|
| 254 |
+
rp_blended = rp * (1.0 - cp_prob[:, None]) + (1.0 / HMM_K) * cp_prob[:, None]
|
| 255 |
+
raw = rp_blended[:, 0] * et + rp_blended[:, 1] * em + rp_blended[:, 2] * ec
|
| 256 |
acc = np.zeros(n); acc[0] = raw[0]
|
| 257 |
for i in range(1, n): acc[i] = lam * acc[i - 1] + (1 - lam) * raw[i]
|
| 258 |
pl = np.array([_sig(4.0 * x) for x in acc])
|
| 259 |
+
return acc, pl, 1.0 - pl, rp_blended
|
| 260 |
|
| 261 |
|
| 262 |
# ---------- Signal + Backtest ----------
|
| 263 |
+
def latest_signal(f, rp_blended, acc, pl, ps, thr, mconf, cp_prob):
|
| 264 |
i = len(f) - 1
|
| 265 |
if i < MIN_SIG: return None
|
| 266 |
+
p = rp_blended[i]; dom = int(np.argmax(p)); conf = float(p[dom])
|
| 267 |
if conf < mconf: return None
|
| 268 |
e, l, s = float(acc[i]), float(pl[i]), float(ps[i])
|
| 269 |
sig = "LONG" if e > thr and l > 0.5 else "SHORT" if e < -thr and s > 0.5 else "NO TRADE"
|
| 270 |
return {"signal": sig, "evidence": e, "P_long": l, "P_short": s,
|
| 271 |
"regime_probs": {REGIME_NAMES[j]: float(p[j]) for j in range(HMM_K)},
|
| 272 |
"dominant_regime": REGIME_NAMES[dom], "regime_conf": conf,
|
| 273 |
+
"changepoint": float(cp_prob[i]), "close": float(f["close"].values[i])}
|
| 274 |
|
| 275 |
|
| 276 |
+
def backtest(sym, f, rp_blended, acc, pl, ps, thr, mconf, maxhold, cp_prob):
|
| 277 |
n = len(f); c = f["close"].values; h = f["high"].values; lo = f["low"].values
|
| 278 |
atr = f["atr"].values
|
| 279 |
trades = []; in_pos = False; ei = eprice = d = sp = tp = 0
|
| 280 |
reg = ""; rconf = 0.0
|
| 281 |
+
start = min(max(int(n * TRAIN_FRAC) + PURGE + EMBARGO, 30), n - 2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
|
| 283 |
for i in range(start, n):
|
| 284 |
if not in_pos:
|
| 285 |
+
p = rp_blended[i]; dom = int(np.argmax(p)); conf = float(p[dom])
|
| 286 |
+
if conf < mconf or cp_prob[i] > 0.5: continue
|
| 287 |
ed = 1 if acc[i] > thr and pl[i] > 0.5 else -1 if acc[i] < -thr and ps[i] > 0.5 else 0
|
| 288 |
if ed == 0 or atr[i] < 1e-12: continue
|
| 289 |
eprice = c[i] * (1.0 + SLIPPAGE * ed); d = ed; ei = i; in_pos = True
|
|
|
|
| 322 |
r = np.array([t["net_return"] for t in trades])
|
| 323 |
w, l = r[r > 0], r[r <= 0]
|
| 324 |
gp = w.sum() if len(w) else 0.0; gl = abs(l.sum()) if len(l) else 0.0
|
| 325 |
+
eq = np.cumprod(1 + r); peak = np.maximum.accumulate(eq); dd = (eq - peak) / peak
|
|
|
|
| 326 |
longs = [t for t in trades if t["direction"] == "LONG"]
|
| 327 |
shorts = [t for t in trades if t["direction"] == "SHORT"]
|
| 328 |
lw = sum(1 for t in longs if t["win"]); sw = sum(1 for t in shorts if t["win"])
|
| 329 |
+
return dict(total_trades=len(r), win_rate=len(w) / len(r),
|
| 330 |
+
profit_factor=gp / gl if gl > 1e-12 else 999.0,
|
| 331 |
net_expectancy=float(r.mean()), avg_win=float(w.mean()) if len(w) else 0.0,
|
| 332 |
avg_loss=float(l.mean()) if len(l) else 0.0,
|
| 333 |
max_drawdown=float(dd.min()) if len(dd) else 0.0,
|
|
|
|
| 344 |
rt = [t for t in trades if t["regime"] == reg]
|
| 345 |
if not rt:
|
| 346 |
rows.append({"Regime": reg, "Trades": 0, "WinRate": 0.0, "PF": 0.0, "NetExp": 0.0}); continue
|
| 347 |
+
n = np.array([t["net_return"] for t in rt]); w, l = n[n > 0], n[n <= 0]
|
|
|
|
| 348 |
gp = w.sum() if len(w) else 0.0; gl = abs(l.sum()) if len(l) else 0.0
|
| 349 |
+
rows.append({"Regime": reg, "Trades": len(rt), "WinRate": round(len(w) / len(rt), 4),
|
|
|
|
| 350 |
"PF": round(gp / gl, 4) if gl > 1e-12 else 999.0,
|
| 351 |
"NetExp": round(float(n.mean()), 6)})
|
| 352 |
return pd.DataFrame(rows)
|
|
|
|
| 357 |
rows = []
|
| 358 |
for sym in sorted({t["symbol"] for t in trades}):
|
| 359 |
st = [t for t in trades if t["symbol"] == sym]
|
| 360 |
+
n = np.array([t["net_return"] for t in st]); w = n[n > 0]
|
| 361 |
+
rows.append({"Symbol": sym, "Trades": len(st), "WinRate": round(len(w) / len(st), 4),
|
| 362 |
+
"NetExp": round(float(n.mean()), 6), "TotalNet": round(float(n.sum()), 6)})
|
|
|
|
|
|
|
|
|
|
| 363 |
return pd.DataFrame(rows).sort_values("TotalNet", ascending=False)
|
| 364 |
|
| 365 |
|
| 366 |
# ---------- Pipeline ----------
|
| 367 |
+
def run(timeframe, n_pairs, thr, lam, mconf, maxhold, progress=gr.Progress()):
|
| 368 |
def err(msg, detail=""):
|
| 369 |
return msg, detail, pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), pd.DataFrame()
|
| 370 |
|
| 371 |
+
n_pairs = max(1, min(12, int(n_pairs)))
|
| 372 |
+
thr = max(0.0, min(1.0, float(thr))); lam = max(0.0, min(0.99, float(lam)))
|
| 373 |
+
mconf = max(0.0, min(1.0, float(mconf))); maxhold = max(5, min(200, int(maxhold)))
|
|
|
|
|
|
|
|
|
|
| 374 |
if timeframe not in TF_MAP:
|
| 375 |
return err("ERROR: Invalid timeframe")
|
| 376 |
|
| 377 |
+
progress(0.02, desc="Selecting forex symbols...")
|
| 378 |
+
pairs = fetch_pairs(n_pairs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
|
| 380 |
signals, trades, analyzed, failed = [], [], 0, []
|
| 381 |
for i, sym in enumerate(pairs):
|
| 382 |
progress((i + 1) / len(pairs) * 0.9, desc=f"{sym} ({i+1}/{len(pairs)})")
|
|
|
|
| 383 |
df = fetch_klines(sym, timeframe)
|
| 384 |
if df is None:
|
|
|
|
| 385 |
failed.append(sym); continue
|
| 386 |
try:
|
| 387 |
+
f = features(df); m = fit_hmm(f)
|
| 388 |
+
if m is None: failed.append(sym); continue
|
|
|
|
|
|
|
|
|
|
| 389 |
rp = forward_filter(m, f)
|
| 390 |
+
cp_prob = bocpd_changepoint_prob(f["ret"].values)
|
| 391 |
+
acc, pl, ps, rp_blended = evidence(f, rp, lam, cp_prob)
|
| 392 |
+
s = latest_signal(f, rp_blended, acc, pl, ps, thr, mconf, cp_prob)
|
| 393 |
if s: s["symbol"] = sym; signals.append(s)
|
| 394 |
+
sym_trades = backtest(sym, f, rp_blended, acc, pl, ps, thr, mconf, maxhold, cp_prob)
|
| 395 |
trades.extend(sym_trades)
|
| 396 |
+
print(f"[RCEA] {sym}: candles={len(f)} cp_recent={cp_prob[-1]:.3f} "
|
| 397 |
+
f"signal={s['signal'] if s else 'NONE'} trades={len(sym_trades)}", flush=True)
|
| 398 |
analyzed += 1
|
| 399 |
except Exception as e:
|
| 400 |
print(f"[RCEA] {sym}: EXC {e}", flush=True); failed.append(sym)
|
| 401 |
progress(1.0, desc="Done")
|
| 402 |
|
| 403 |
+
scan = ["# RCEA Lite — Forex + BOCPD",
|
| 404 |
+
f"**Timeframe:** {timeframe}",
|
| 405 |
+
f"**Pairs requested:** {n_pairs}",
|
| 406 |
f"**Analyzed:** {analyzed}",
|
| 407 |
f"**Failed:** {len(failed)}",
|
| 408 |
f"**Total trades:** {len(trades)}",
|
| 409 |
f"**Threshold:** {thr} | **λ:** {lam} | **MinConf:** {mconf} | **MaxHold:** {maxhold}",
|
| 410 |
f"**UTC:** {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')}"]
|
| 411 |
if len(trades) < 30:
|
| 412 |
+
scan.append(f"\n⚠️ Only {len(trades)} trades. Try threshold=0.04, λ=0.65, MinConf=0.25.")
|
|
|
|
| 413 |
scan_report = "\n".join(scan)
|
| 414 |
|
| 415 |
if signals:
|
| 416 |
sig_df = pd.DataFrame([{
|
| 417 |
"Symbol": s["symbol"], "Signal": s["signal"],
|
| 418 |
+
"Evidence": round(s["evidence"], 4), "P(Long)": round(s["P_long"], 4),
|
| 419 |
+
"P(Short)": round(s["P_short"], 4), "Regime": s["dominant_regime"],
|
| 420 |
+
"Regime Conf": round(s["regime_conf"], 4), "Changepoint": round(s["changepoint"], 4),
|
| 421 |
"P(Trend)": round(s["regime_probs"]["TREND"], 4),
|
| 422 |
"P(MeanRev)": round(s["regime_probs"]["MEAN_REVERSION"], 4),
|
| 423 |
"P(Compress)": round(s["regime_probs"]["COMPRESSION"], 4),
|
|
|
|
| 426 |
sig_df = sig_df.sort_values(["_p", "Evidence"], ascending=[True, False]).drop(columns="_p")
|
| 427 |
else:
|
| 428 |
sig_df = pd.DataFrame(columns=["Symbol", "Signal", "Evidence", "P(Long)", "P(Short)",
|
| 429 |
+
"Regime", "Regime Conf", "Changepoint", "P(Trend)",
|
| 430 |
+
"P(MeanRev)", "P(Compress)", "Close"])
|
| 431 |
|
| 432 |
if trades:
|
| 433 |
tr_df = pd.DataFrame([{
|
| 434 |
+
"Symbol": t["symbol"], "Direction": t["direction"], "Entry Bar": t["entry_bar"],
|
| 435 |
+
"Exit Bar": t["exit_bar"], "Bars": t["bars_held"],
|
|
|
|
| 436 |
"Entry": round(t["entry_price"], 6), "Exit": round(t["exit_price"], 6),
|
| 437 |
"Stop": round(t["stop_price"], 6), "Target": round(t["target_price"], 6),
|
| 438 |
+
"Reason": t["exit_reason"], "Gross": round(t["gross_return"], 6),
|
| 439 |
+
"Net": round(t["net_return"], 6), "Regime": t["regime"],
|
| 440 |
+
"Regime Conf": round(t["regime_conf"], 4), "Win": t["win"]} for t in trades]
|
| 441 |
+
).sort_values(["Symbol", "Entry Bar"])
|
| 442 |
else:
|
| 443 |
tr_df = pd.DataFrame(columns=["Symbol", "Direction", "Entry Bar", "Exit Bar", "Bars",
|
| 444 |
"Entry", "Exit", "Stop", "Target", "Reason",
|
|
|
|
| 447 |
m = metrics(trades)
|
| 448 |
m_str = "\n".join([
|
| 449 |
"# Performance Metrics (net of costs)",
|
| 450 |
+
f"**Total trades:** {m['total_trades']}", f"**Win rate:** {m['win_rate']:.2%}",
|
| 451 |
+
f"**Profit factor:** {m['profit_factor']:.3f}", f"**Net expectancy:** {m['net_expectancy']:.6f}",
|
|
|
|
|
|
|
| 452 |
f"**Avg win / loss:** {m['avg_win']:.6f} / {m['avg_loss']:.6f}",
|
| 453 |
+
f"**Max DD:** {m['max_drawdown']:.2%}", f"**Net compounded:** {m['net_compounded_return']:.2%}",
|
|
|
|
| 454 |
f"**Long:** {m['long_trades']} ({m['long_win_rate']:.2%}) | "
|
| 455 |
f"**Short:** {m['short_trades']} ({m['short_win_rate']:.2%})",
|
| 456 |
])
|
|
|
|
| 461 |
|
| 462 |
|
| 463 |
# ---------- UI ----------
|
| 464 |
+
with gr.Blocks(title="RCEA Lite v1.0 — Forex") as demo:
|
| 465 |
+
gr.Markdown("# RCEA Lite v1.0 — Forex Edition\n"
|
| 466 |
+
"HMM + BOCPD · Causal · No supervised ML · Data: GitHub CSV (no API key)")
|
| 467 |
with gr.Row():
|
| 468 |
with gr.Column(scale=1):
|
| 469 |
tf = gr.Dropdown(["15m", "1h", "4h", "1d"], value="1h", label="Timeframe")
|
| 470 |
+
nc = gr.Slider(1, 12, value=12, step=1, label="Pairs")
|
| 471 |
thr_i = gr.Slider(0.0, 1.0, value=0.06, step=0.01, label="Evidence threshold")
|
| 472 |
lam_i = gr.Slider(0.0, 0.99, value=0.70, step=0.01, label="Memory λ")
|
| 473 |
mc_i = gr.Slider(0.0, 1.0, value=0.30, step=0.01, label="Min regime confidence")
|
|
|
|
| 480 |
with gr.Tab("📡 Signals"): sig_tbl = gr.Dataframe(interactive=False, wrap=True)
|
| 481 |
with gr.Tab("📜 Trades"): tr_tbl = gr.Dataframe(interactive=False, wrap=True)
|
| 482 |
with gr.Tab("📊 Regime"): reg_tbl = gr.Dataframe(interactive=False)
|
| 483 |
+
with gr.Tab("🪙 Pairs"): coin_tbl = gr.Dataframe(interactive=False)
|
| 484 |
btn.click(run, inputs=[tf, nc, thr_i, lam_i, mc_i, mh_i],
|
| 485 |
outputs=[scan_out, met_out, sig_tbl, tr_tbl, reg_tbl, coin_tbl])
|
| 486 |
+
gr.Markdown("---\n**Data:** ejtraderLabs/historical-data (12 forex pairs, 10y OHLCV). "
|
| 487 |
+
"Net of 0.02% commission + 0.01% slippage per side.")
|
| 488 |
|
| 489 |
|
| 490 |
if __name__ == "__main__":
|