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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +176 -168
app.py CHANGED
@@ -1,50 +1,49 @@
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 ----------
41
- def _f(x):
42
- try:
43
- v = float(x); return v if math.isfinite(v) else None
44
- except (TypeError, ValueError):
45
- return None
46
 
47
 
 
48
  def _sig(x):
49
  return 1.0 / (1.0 + math.exp(-x)) if x >= 0 else math.exp(x) / (1.0 + math.exp(x))
50
 
@@ -72,105 +71,79 @@ def _rsi(c, p=14):
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,7 +165,8 @@ def features(df):
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,7 +187,8 @@ def fit_hmm(f):
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,9 +209,10 @@ def forward_filter(m, f):
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,39 +227,51 @@ def evidence(f, rp, lam, cp_prob):
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,12 +310,13 @@ def metrics(trades):
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,9 +333,11 @@ def regime_table(trades):
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,67 +348,80 @@ def coin_table(trades):
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,19 +430,20 @@ def run(timeframe, n_pairs, thr, lam, mconf, maxhold, progress=gr.Progress()):
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,10 +452,13 @@ def run(timeframe, n_pairs, thr, lam, mconf, maxhold, progress=gr.Progress()):
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,13 +469,13 @@ def run(timeframe, n_pairs, thr, lam, mconf, maxhold, progress=gr.Progress()):
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,11 +488,11 @@ with gr.Blocks(title="RCEA Lite v1.0 — Forex") as demo:
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__":
 
1
  """
2
+ RCEA Lite v1.0 — yfinance 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 yfinance as yf
13
  import gradio as gr
14
  from hmmlearn import hmm
15
 
16
  warnings.filterwarnings("ignore")
17
 
18
  # ---------- Config ----------
19
+ COMMISSION, SLIPPAGE = 0.001, 0.0005
 
 
 
 
 
 
 
20
  COST = 2 * (COMMISSION + SLIPPAGE)
21
  REGIME_NAMES = ["TREND", "MEAN_REVERSION", "COMPRESSION"]
22
  HMM_K = 3
23
  TRAIN_FRAC, PURGE, EMBARGO = 0.60, 5, 3
24
  MIN_HMM, MIN_SIG = 150, 80
25
  FEAT_COLS = ["ret", "atr_norm", "ema_diff_norm", "efficiency", "rsi_dev", "dist_ma"]
 
26
 
27
+ # 30 USDT paritesi (yfinance sembolleri)
28
+ COINS = [
29
+ "BTC-USD", "ETH-USD", "BNB-USD", "SOL-USD", "XRP-USD",
30
+ "ADA-USD", "DOGE-USD", "AVAX-USD", "TRX-USD", "DOT-USD",
31
+ "LINK-USD", "MATIC-USD", "LTC-USD", "BCH-USD", "UNI-USD",
32
+ "ATOM-USD", "ETC-USD", "XLM-USD", "NEAR-USD", "APT-USD",
33
+ "ARB-USD", "OP-USD", "FIL-USD", "INJ-USD", "SUI-USD",
34
+ "SEI-USD", "TIA-USD", "AAVE-USD", "MKR-USD", "GRT-USD",
35
+ ]
36
 
37
+ # yfinance interval mapping + max period per interval
38
+ YF_TF = {
39
+ "15m": ("15m", "60d"),
40
+ "1h": ("1h", "730d"),
41
+ "4h": ("1h", "730d"), # 4h yok → 1h'den resample
42
+ "1d": ("1d", "5y"),
43
+ }
44
 
45
 
46
+ # ---------- Helpers ----------
47
  def _sig(x):
48
  return 1.0 / (1.0 + math.exp(-x)) if x >= 0 else math.exp(x) / (1.0 + math.exp(x))
49
 
 
71
  return 100.0 - 100.0 / (1.0 + rs)
72
 
73
 
74
+ # ---------- yfinance Data Layer ----------
75
+ def fetch_pairs(n=30):
76
+ """Return fixed 30-coin USDT list (yfinance)."""
77
+ out = COINS[:max(30, min(30, int(n)))]
78
+ print(f"[RCEA] {len(out)} yfinance pairs resolved", flush=True)
79
+ return out
 
 
80
 
81
 
82
+ def fetch_klines(inst_id, timeframe, target_bars=1000):
83
+ """
84
+ Fetch OHLCV from Yahoo Finance via yfinance.
85
+ For 4h, downloads 1h and resamples to 4H.
86
+ Returns None if insufficient data.
87
+ """
88
+ if timeframe not in YF_TF:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  return None
90
+ interval, period = YF_TF[timeframe]
91
+
92
  try:
93
+ raw = yf.download(
94
+ inst_id,
95
+ interval=interval,
96
+ period=period,
97
+ auto_adjust=False,
98
+ progress=False,
99
+ threads=False,
100
+ )
101
  except Exception as e:
102
+ print(f"[RCEA] {inst_id} download failed: {e}", flush=True)
103
+ return None
104
+
105
+ if raw is None or raw.empty:
106
+ print(f"[RCEA] {inst_id}: empty data", flush=True)
107
  return None
108
 
109
+ # yfinance multiindex flatten (tek sembolde bile MultiIndex dönebilir)
110
+ if isinstance(raw.columns, pd.MultiIndex):
111
+ raw.columns = raw.columns.get_level_values(0)
 
 
112
 
113
+ raw = raw.rename(columns=str.lower)
114
+ needed = ["open", "high", "low", "close", "volume"]
115
+ if not all(c in raw.columns for c in needed):
116
+ print(f"[RCEA] {inst_id}: missing columns {raw.columns.tolist()}", flush=True)
117
  return None
118
 
119
+ df = raw[needed].copy()
120
+ df = df.dropna(subset=["open", "high", "low", "close"])
121
+ df = df[df["close"] > 0]
122
+
123
+ # 4h resample
124
+ if timeframe == "4h":
125
+ df = df.resample("4H").agg({
126
+ "open": "first", "high": "max", "low": "min",
127
+ "close": "last", "volume": "sum",
128
+ }).dropna()
129
 
 
 
 
130
  if len(df) < MIN_HMM:
131
+ print(f"[RCEA] {inst_id}: only {len(df)} bars (< {MIN_HMM})", flush=True)
132
  return None
133
+
134
+ # Son target_bars
135
+ df = df.tail(target_bars).reset_index(drop=True)
136
+ return df
137
 
138
 
139
  # ---------- Features ----------
140
  def features(df):
141
+ c = df["close"].values.astype(float)
142
+ h = df["high"].values.astype(float)
143
+ lo = df["low"].values.astype(float)
144
+ n = len(c)
145
+ o = pd.DataFrame(index=df.index)
146
+ o["close"] = c
147
  r = np.zeros(n); r[1:] = np.diff(c) / c[:-1]; o["ret"] = r
148
  a14 = _atr(h, lo, c, 14); o["atr"] = a14
149
  o["atr_norm"] = a14 / np.where(c > 0, c, 1.0)
 
165
  ae = np.where(a14 > 1e-12, (c - rm) / a14, 0.0); o["atr_extension"] = np.nan_to_num(ae)
166
  a5, a20 = _atr(h, lo, c, 5), _atr(h, lo, c, 20)
167
  o["vol_ratio"] = np.where(a20 > 1e-12, a5 / a20, 1.0)
168
+ ph, pl_ = np.roll(h, 1), np.roll(lo, 1)
169
+ ph[0], pl_[0] = h[0], lo[0]
170
  o["prev_high"], o["prev_low"] = ph, pl_
171
  o["breakout"] = np.where(c > ph, 1.0, np.where(c < pl_, -1.0, 0.0))
172
  return o.replace([np.inf, -np.inf], np.nan).fillna(0.0)
 
187
 
188
 
189
  def forward_filter(m, f):
190
+ n = len(f)
191
+ post = np.ones((n, HMM_K)) / HMM_K
192
  ntr = min(max(MIN_HMM, int(n * TRAIN_FRAC)), n - 20)
193
  if ntr < 10: return post
194
  X = (f[FEAT_COLS].values - m._mu) / m._sd
 
209
  return post
210
 
211
 
212
+ # ---------- Evidence ----------
213
+ def evidence(f, rp, lam):
214
+ n = len(f)
215
+ c = f["close"].values; ef = f["ema_fast"].values; es = f["ema_slow"].values
216
  eff = f["efficiency"].values; mom = f["momentum"].values; am = f["atr_move"].values
217
  rd = f["rsi_dev"].values; dm = f["dist_ma"].values; ae = f["atr_extension"].values
218
  vr = f["vol_ratio"].values; bo = f["breakout"].values
 
227
  em[i] = np.tanh(s * 2.0)
228
  contr = max(0.0, 1.0 - vr[i])
229
  ec[i] = np.tanh(bo[i] * (0.5 + 0.5 * contr) * 2.0)
230
+ raw = rp[:, 0] * et + rp[:, 1] * em + rp[:, 2] * ec
 
231
  acc = np.zeros(n); acc[0] = raw[0]
232
  for i in range(1, n): acc[i] = lam * acc[i - 1] + (1 - lam) * raw[i]
233
  pl = np.array([_sig(4.0 * x) for x in acc])
234
+ return acc, pl, 1.0 - pl
235
 
236
 
237
  # ---------- Signal + Backtest ----------
238
+ def latest_signal(f, rp, acc, pl, ps, thr, mconf):
239
  i = len(f) - 1
240
  if i < MIN_SIG: return None
241
+ p = rp[i]; dom = int(np.argmax(p)); conf = float(p[dom])
242
  if conf < mconf: return None
243
  e, l, s = float(acc[i]), float(pl[i]), float(ps[i])
244
  sig = "LONG" if e > thr and l > 0.5 else "SHORT" if e < -thr and s > 0.5 else "NO TRADE"
245
  return {"signal": sig, "evidence": e, "P_long": l, "P_short": s,
246
  "regime_probs": {REGIME_NAMES[j]: float(p[j]) for j in range(HMM_K)},
247
  "dominant_regime": REGIME_NAMES[dom], "regime_conf": conf,
248
+ "close": float(f["close"].values[i])}
249
 
250
 
251
+ def backtest(sym, f, rp, acc, pl, ps, thr, mconf, maxhold):
252
  n = len(f); c = f["close"].values; h = f["high"].values; lo = f["low"].values
253
  atr = f["atr"].values
254
  trades = []; in_pos = False; ei = eprice = d = sp = tp = 0
255
  reg = ""; rconf = 0.0
256
+
257
+ train_end = int(n * TRAIN_FRAC)
258
+ start = train_end + PURGE + EMBARGO
259
+ start = max(start, 30)
260
+ start = min(start, n - 2)
261
+
262
+ if start < n:
263
+ pass_thr = int((np.abs(acc[start:]) > thr).sum())
264
+ pass_conf = int((rp[start:].max(axis=1) >= mconf).sum())
265
+ both = int(((np.abs(acc[start:]) > thr) &
266
+ (rp[start:].max(axis=1) >= mconf)).sum())
267
+ print(f"[BT] {sym}: n={n} start={start} window={n-start} "
268
+ f"thr_pass={pass_thr} conf_pass={pass_conf} both={both}",
269
+ flush=True)
270
 
271
  for i in range(start, n):
272
  if not in_pos:
273
+ p = rp[i]; dom = int(np.argmax(p)); conf = float(p[dom])
274
+ if conf < mconf: continue
275
  ed = 1 if acc[i] > thr and pl[i] > 0.5 else -1 if acc[i] < -thr and ps[i] > 0.5 else 0
276
  if ed == 0 or atr[i] < 1e-12: continue
277
  eprice = c[i] * (1.0 + SLIPPAGE * ed); d = ed; ei = i; in_pos = True
 
310
  r = np.array([t["net_return"] for t in trades])
311
  w, l = r[r > 0], r[r <= 0]
312
  gp = w.sum() if len(w) else 0.0; gl = abs(l.sum()) if len(l) else 0.0
313
+ eq = np.cumprod(1 + r); peak = np.maximum.accumulate(eq)
314
+ dd = (eq - peak) / peak
315
  longs = [t for t in trades if t["direction"] == "LONG"]
316
  shorts = [t for t in trades if t["direction"] == "SHORT"]
317
  lw = sum(1 for t in longs if t["win"]); sw = sum(1 for t in shorts if t["win"])
318
+ return dict(total_trades=len(r),
319
+ win_rate=len(w) / len(r), profit_factor=gp / gl if gl > 1e-12 else 999.0,
320
  net_expectancy=float(r.mean()), avg_win=float(w.mean()) if len(w) else 0.0,
321
  avg_loss=float(l.mean()) if len(l) else 0.0,
322
  max_drawdown=float(dd.min()) if len(dd) else 0.0,
 
333
  rt = [t for t in trades if t["regime"] == reg]
334
  if not rt:
335
  rows.append({"Regime": reg, "Trades": 0, "WinRate": 0.0, "PF": 0.0, "NetExp": 0.0}); continue
336
+ n = np.array([t["net_return"] for t in rt])
337
+ w, l = n[n > 0], n[n <= 0]
338
  gp = w.sum() if len(w) else 0.0; gl = abs(l.sum()) if len(l) else 0.0
339
+ rows.append({"Regime": reg, "Trades": len(rt),
340
+ "WinRate": round(len(w) / len(rt), 4),
341
  "PF": round(gp / gl, 4) if gl > 1e-12 else 999.0,
342
  "NetExp": round(float(n.mean()), 6)})
343
  return pd.DataFrame(rows)
 
348
  rows = []
349
  for sym in sorted({t["symbol"] for t in trades}):
350
  st = [t for t in trades if t["symbol"] == sym]
351
+ n = np.array([t["net_return"] for t in st])
352
+ w = n[n > 0]
353
+ rows.append({"Symbol": sym, "Trades": len(st),
354
+ "WinRate": round(len(w) / len(st), 4),
355
+ "NetExp": round(float(n.mean()), 6),
356
+ "TotalNet": round(float(n.sum()), 6)})
357
  return pd.DataFrame(rows).sort_values("TotalNet", ascending=False)
358
 
359
 
360
  # ---------- Pipeline ----------
361
+ def run(timeframe, n_coins, thr, lam, mconf, maxhold, progress=gr.Progress()):
362
  def err(msg, detail=""):
363
  return msg, detail, pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), pd.DataFrame()
364
 
365
+ n_coins = max(30, min(30, int(n_coins))) # sabit 30
366
+ thr = max(0.0, min(1.0, float(thr)))
367
+ lam = max(0.0, min(0.99, float(lam)))
368
+ mconf = max(0.0, min(1.0, float(mconf)))
369
+ maxhold = max(5, min(200, int(maxhold)))
370
+
371
+ if timeframe not in YF_TF:
372
  return err("ERROR: Invalid timeframe")
373
 
374
+ progress(0.02, desc="Resolving yfinance pairs...")
375
+ try: pairs = fetch_pairs(n_coins)
376
+ except Exception as e: return err(f"ERROR fetching pairs: {e}")
377
+ if not pairs:
378
+ return err("ERROR: No pairs resolved.")
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
+ time.sleep(0.25) # yfinance rate-limit nezaketi
384
  df = fetch_klines(sym, timeframe)
385
  if df is None:
386
  failed.append(sym); continue
387
  try:
388
+ f = features(df)
389
+ m = fit_hmm(f)
390
+ if m is None:
391
+ print(f"[RCEA] {sym}: HMM fit failed (n={len(f)})", flush=True)
392
+ failed.append(sym); continue
393
  rp = forward_filter(m, f)
394
+ acc, pl, ps = evidence(f, rp, lam)
395
+ s = latest_signal(f, rp, acc, pl, ps, thr, mconf)
 
396
  if s: s["symbol"] = sym; signals.append(s)
397
+ sym_trades = backtest(sym, f, rp, acc, pl, ps, thr, mconf, maxhold)
398
  trades.extend(sym_trades)
399
+ print(f"[RCEA] {sym}: candles={len(f)} signal={s['signal'] if s else 'NONE'} "
400
+ f"trades={len(sym_trades)}", flush=True)
401
  analyzed += 1
402
  except Exception as e:
403
  print(f"[RCEA] {sym}: EXC {e}", flush=True); failed.append(sym)
404
  progress(1.0, desc="Done")
405
 
406
+ scan = ["# RCEA Lite — yfinance Market Scan",
407
+ f"**Timeframe:** {timeframe} | **yfinance interval:** {YF_TF[timeframe][0]}",
408
+ f"**Coins:** {len(pairs)} (fixed list)",
409
  f"**Analyzed:** {analyzed}",
410
  f"**Failed:** {len(failed)}",
411
  f"**Total trades:** {len(trades)}",
412
  f"**Threshold:** {thr} | **λ:** {lam} | **MinConf:** {mconf} | **MaxHold:** {maxhold}",
413
  f"**UTC:** {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')}"]
414
  if len(trades) < 30:
415
+ scan.append(f"\n⚠️ Only {len(trades)} trades. Try threshold=0.04, "
416
+ f"λ=0.65, MinConf=0.25.")
417
  scan_report = "\n".join(scan)
418
 
419
  if signals:
420
  sig_df = pd.DataFrame([{
421
  "Symbol": s["symbol"], "Signal": s["signal"],
422
+ "Evidence": round(s["evidence"], 4),
423
+ "P(Long)": round(s["P_long"], 4), "P(Short)": round(s["P_short"], 4),
424
+ "Regime": s["dominant_regime"], "Regime Conf": round(s["regime_conf"], 4),
425
  "P(Trend)": round(s["regime_probs"]["TREND"], 4),
426
  "P(MeanRev)": round(s["regime_probs"]["MEAN_REVERSION"], 4),
427
  "P(Compress)": round(s["regime_probs"]["COMPRESSION"], 4),
 
430
  sig_df = sig_df.sort_values(["_p", "Evidence"], ascending=[True, False]).drop(columns="_p")
431
  else:
432
  sig_df = pd.DataFrame(columns=["Symbol", "Signal", "Evidence", "P(Long)", "P(Short)",
433
+ "Regime", "Regime Conf", "P(Trend)", "P(MeanRev)",
434
+ "P(Compress)", "Close"])
435
 
436
  if trades:
437
  tr_df = pd.DataFrame([{
438
+ "Symbol": t["symbol"], "Direction": t["direction"],
439
+ "Entry Bar": t["entry_bar"], "Exit Bar": t["exit_bar"],
440
+ "Bars": t["bars_held"],
441
  "Entry": round(t["entry_price"], 6), "Exit": round(t["exit_price"], 6),
442
  "Stop": round(t["stop_price"], 6), "Target": round(t["target_price"], 6),
443
+ "Reason": t["exit_reason"],
444
+ "Gross": round(t["gross_return"], 6), "Net": round(t["net_return"], 6),
445
+ "Regime": t["regime"], "Regime Conf": round(t["regime_conf"], 4),
446
+ "Win": t["win"]} for t in trades]).sort_values(["Symbol", "Entry Bar"])
447
  else:
448
  tr_df = pd.DataFrame(columns=["Symbol", "Direction", "Entry Bar", "Exit Bar", "Bars",
449
  "Entry", "Exit", "Stop", "Target", "Reason",
 
452
  m = metrics(trades)
453
  m_str = "\n".join([
454
  "# Performance Metrics (net of costs)",
455
+ f"**Total trades:** {m['total_trades']}",
456
+ f"**Win rate:** {m['win_rate']:.2%}",
457
+ f"**Profit factor:** {m['profit_factor']:.3f}",
458
+ f"**Net expectancy:** {m['net_expectancy']:.6f}",
459
  f"**Avg win / loss:** {m['avg_win']:.6f} / {m['avg_loss']:.6f}",
460
+ f"**Max DD:** {m['max_drawdown']:.2%}",
461
+ f"**Net compounded:** {m['net_compounded_return']:.2%}",
462
  f"**Long:** {m['long_trades']} ({m['long_win_rate']:.2%}) | "
463
  f"**Short:** {m['short_trades']} ({m['short_win_rate']:.2%})",
464
  ])
 
469
 
470
 
471
  # ---------- UI ----------
472
+ with gr.Blocks(title="RCEA Lite v1.0 — yfinance") as demo:
473
+ gr.Markdown("# RCEA Lite v1.0 — yfinance Edition\n"
474
+ "HMM regime detection (causal forward filtering) · No supervised ML · Data: Yahoo Finance")
475
  with gr.Row():
476
  with gr.Column(scale=1):
477
  tf = gr.Dropdown(["15m", "1h", "4h", "1d"], value="1h", label="Timeframe")
478
+ nc = gr.Slider(30, 30, value=30, step=1, label="Coins (fixed 30)")
479
  thr_i = gr.Slider(0.0, 1.0, value=0.06, step=0.01, label="Evidence threshold")
480
  lam_i = gr.Slider(0.0, 0.99, value=0.70, step=0.01, label="Memory λ")
481
  mc_i = gr.Slider(0.0, 1.0, value=0.30, step=0.01, label="Min regime confidence")
 
488
  with gr.Tab("📡 Signals"): sig_tbl = gr.Dataframe(interactive=False, wrap=True)
489
  with gr.Tab("📜 Trades"): tr_tbl = gr.Dataframe(interactive=False, wrap=True)
490
  with gr.Tab("📊 Regime"): reg_tbl = gr.Dataframe(interactive=False)
491
+ with gr.Tab("🪙 Coins"): coin_tbl = gr.Dataframe(interactive=False)
492
  btn.click(run, inputs=[tf, nc, thr_i, lam_i, mc_i, mh_i],
493
  outputs=[scan_out, met_out, sig_tbl, tr_tbl, reg_tbl, coin_tbl])
494
+ gr.Markdown("---\n**Data source:** Yahoo Finance (yfinance). "
495
+ "Net of 0.10% commission + 0.05% slippage per side.")
496
 
497
 
498
  if __name__ == "__main__":