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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +167 -233
app.py CHANGED
@@ -1,35 +1,40 @@
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
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
- OKX_BASE = "https://www.okx.com"
20
- TIMEOUT, RETRIES, DELAY = 20, 3, 1.5
21
- COMMISSION, SLIPPAGE = 0.001, 0.0005
 
 
 
 
 
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
- TF_MAP = {"15m": "15m", "1h": "1H", "4h": "4H", "1d": "1D"}
32
- TARGET_BARS = 1000
 
 
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
- # ---------- OKX API ----------
71
- def _req(path, params, timeout=TIMEOUT):
72
- last = None
73
- for attempt in range(RETRIES):
74
- try:
75
- r = requests.get(OKX_BASE + path, params=params, timeout=timeout,
76
- headers={"User-Agent": "RCEA-Lite/1.0"})
77
- if r.status_code == 200:
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
- 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
 
191
  # ---------- Features ----------
192
  def features(df):
193
- c = df["close"].values.astype(float)
194
- h = df["high"].values.astype(float)
195
- lo = df["low"].values.astype(float)
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
- 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
@@ -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
- win_rate=len(w) / len(r), profit_factor=gp / gl if gl > 1e-12 else 999.0,
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
- w = n[n > 0]
408
- rows.append({"Symbol": sym, "Trades": len(st),
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, n_coins, thr, lam, mconf, maxhold, progress=gr.Progress()):
417
  def err(msg, detail=""):
418
  return msg, detail, pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), pd.DataFrame()
419
 
420
- n_coins = max(30, min(100, int(n_coins)))
421
- thr = max(0.0, min(1.0, float(thr)))
422
- lam = max(0.0, min(0.99, float(lam)))
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="Fetching OKX USDT pairs...")
430
- try: pairs = fetch_pairs(n_coins)
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 = fit_hmm(f)
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
- 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, "
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(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,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", "P(Trend)", "P(MeanRev)",
492
- "P(Compress)", "Close"])
493
 
494
  if trades:
495
  tr_df = pd.DataFrame([{
496
- "Symbol": t["symbol"], "Direction": t["direction"],
497
- "Entry Bar": t["entry_bar"], "Exit Bar": t["exit_bar"],
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
- "Gross": round(t["gross_return"], 6), "Net": round(t["net_return"], 6),
503
- "Regime": t["regime"], "Regime Conf": round(t["regime_conf"], 4),
504
- "Win": t["win"]} for t in trades]).sort_values(["Symbol", "Entry Bar"])
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"**Win rate:** {m['win_rate']:.2%}",
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 — 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")
536
- nc = gr.Slider(30, 100, value=50, step=1, label="Coins")
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("🪙 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__":
 
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__":