GoshawkVortexAI commited on
Commit
b662b7d
·
verified ·
1 Parent(s): 436fdcd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +93 -176
app.py CHANGED
@@ -1,7 +1,6 @@
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,14 +31,6 @@ FEAT_COLS = ["ret", "atr_norm", "ema_diff_norm", "efficiency", "rsi_dev", "dist_
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,99 +67,6 @@ def _rsi(c, p=14):
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,29 +93,42 @@ def _req(path, params, timeout=TIMEOUT):
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,30 +136,55 @@ def fetch_pairs(n=30):
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,13 +261,8 @@ def forward_filter(m, f):
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,59 +279,54 @@ def evidence(f, rp, lam, cp_prob):
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,32 +449,25 @@ def run(timeframe, n_coins, thr, lam, mconf, maxhold, progress=gr.Progress()):
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,7 +480,6 @@ def run(timeframe, n_coins, thr, lam, mconf, maxhold, progress=gr.Progress()):
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,8 +488,8 @@ def run(timeframe, n_coins, thr, lam, mconf, maxhold, progress=gr.Progress()):
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,9 +527,9 @@ def run(timeframe, n_coins, thr, lam, mconf, maxhold, progress=gr.Progress()):
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,9 +549,8 @@ with gr.Blocks(title="RCEA Lite v1.0 — OKX + BOCPD") as demo:
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__":
 
1
  """
2
+ RCEA Lite v1.0 — OKX Edition
3
+ Regime-Conditioned Evidence Accumulation. Causal HMM. No supervised ML.
 
4
  """
5
 
6
  import math, time, warnings
 
31
  TF_MAP = {"15m": "15m", "1h": "1H", "4h": "4H", "1d": "1D"}
32
  TARGET_BARS = 1000
33
 
 
 
 
 
 
 
 
 
34
 
35
  # ---------- Helpers ----------
36
  def _f(x):
 
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
 
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)
 
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
+ if len(all_rows) >= target_bars:
178
+ break
179
  time.sleep(0.12)
180
+
181
+ if len(all_rows) < MIN_HMM:
182
+ return None
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
  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
  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
+ raw = rp[:, 0] * et + rp[:, 1] * em + rp[:, 2] * ec
 
 
 
 
 
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 = rp[i]; dom = int(np.argmax(p)); conf = float(p[dom])
 
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) # 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 = rp[i]; dom = int(np.argmax(p)); conf = float(p[dom])
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
  print(f"[RCEA] {sym}: HMM fit failed (n={len(f)})", flush=True)
450
  failed.append(sym); continue
451
  rp = forward_filter(m, f)
452
+ acc, pl, ps = evidence(f, rp, lam)
453
+ s = latest_signal(f, rp, acc, pl, ps, thr, mconf)
 
 
 
454
  if s: s["symbol"] = sym; signals.append(s)
455
+ sym_trades = backtest(sym, f, rp, acc, pl, ps, thr, mconf, maxhold)
 
456
  trades.extend(sym_trades)
457
+ print(f"[RCEA] {sym}: candles={len(f)} signal={s['signal'] if s else 'NONE'} "
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 — OKX Market Scan",
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
  "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
  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", "P(Trend)", "P(MeanRev)",
492
+ "P(Compress)", "Close"])
493
 
494
  if trades:
495
  tr_df = pd.DataFrame([{
 
527
 
528
 
529
  # ---------- UI ----------
530
+ with gr.Blocks(title="RCEA Lite v1.0 — OKX") as demo:
531
+ gr.Markdown("# RCEA Lite v1.0 — OKX Edition\n"
532
+ "HMM regime detection (causal forward filtering) · No supervised ML · Data: OKX")
533
  with gr.Row():
534
  with gr.Column(scale=1):
535
  tf = gr.Dropdown(["15m", "1h", "4h", "1d"], value="1h", label="Timeframe")
 
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**Data source:** OKX public REST API. "
553
+ "Net of 0.10% commission + 0.05% slippage per side.")
 
554
 
555
 
556
  if __name__ == "__main__":