Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,50 +1,49 @@
|
|
| 1 |
"""
|
| 2 |
-
RCEA Lite v1.0 —
|
| 3 |
-
Regime-Conditioned Evidence Accumulation
|
| 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 |
-
|
| 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 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 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 |
-
# ----------
|
| 76 |
-
def
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 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
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 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 |
-
|
| 133 |
-
|
| 134 |
try:
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
except Exception as e:
|
| 137 |
-
print(f"[RCEA] {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
return None
|
| 139 |
|
| 140 |
-
|
| 141 |
-
if
|
| 142 |
-
|
| 143 |
-
elif "time" in df.columns:
|
| 144 |
-
df = df.rename(columns={"time": "datetime"})
|
| 145 |
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
|
|
|
| 149 |
return None
|
| 150 |
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
|
|
|
|
|
|
| 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] {
|
| 165 |
return None
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
| 167 |
|
| 168 |
|
| 169 |
# ---------- Features ----------
|
| 170 |
def features(df):
|
| 171 |
-
c = df["close"].values.astype(float)
|
| 172 |
-
|
| 173 |
-
|
|
|
|
|
|
|
|
|
|
| 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)
|
|
|
|
| 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)
|
|
|
|
| 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
|
| 238 |
-
def evidence(f, rp, lam
|
| 239 |
-
n = len(f)
|
|
|
|
| 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 |
-
|
| 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
|
| 260 |
|
| 261 |
|
| 262 |
# ---------- Signal + Backtest ----------
|
| 263 |
-
def latest_signal(f,
|
| 264 |
i = len(f) - 1
|
| 265 |
if i < MIN_SIG: return None
|
| 266 |
-
p =
|
| 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 |
-
"
|
| 274 |
|
| 275 |
|
| 276 |
-
def backtest(sym, f,
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
|
| 283 |
for i in range(start, n):
|
| 284 |
if not in_pos:
|
| 285 |
-
p =
|
| 286 |
-
if conf < mconf
|
| 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)
|
|
|
|
| 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),
|
| 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])
|
|
|
|
| 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),
|
|
|
|
| 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])
|
| 361 |
-
|
| 362 |
-
|
|
|
|
|
|
|
|
|
|
| 363 |
return pd.DataFrame(rows).sort_values("TotalNet", ascending=False)
|
| 364 |
|
| 365 |
|
| 366 |
# ---------- Pipeline ----------
|
| 367 |
-
def run(timeframe,
|
| 368 |
def err(msg, detail=""):
|
| 369 |
return msg, detail, pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), pd.DataFrame()
|
| 370 |
|
| 371 |
-
|
| 372 |
-
thr = max(0.0, min(1.0, float(thr)))
|
| 373 |
-
|
| 374 |
-
|
|
|
|
|
|
|
|
|
|
| 375 |
return err("ERROR: Invalid timeframe")
|
| 376 |
|
| 377 |
-
progress(0.02, desc="
|
| 378 |
-
pairs = fetch_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)
|
| 388 |
-
|
|
|
|
|
|
|
|
|
|
| 389 |
rp = forward_filter(m, f)
|
| 390 |
-
|
| 391 |
-
|
| 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,
|
| 395 |
trades.extend(sym_trades)
|
| 396 |
-
print(f"[RCEA] {sym}: candles={len(f)}
|
| 397 |
-
f"
|
| 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 —
|
| 404 |
-
f"**Timeframe:** {timeframe}",
|
| 405 |
-
f"**
|
| 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,
|
|
|
|
| 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),
|
| 419 |
-
"P(
|
| 420 |
-
"Regime
|
| 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", "
|
| 430 |
-
"P(
|
| 431 |
|
| 432 |
if trades:
|
| 433 |
tr_df = pd.DataFrame([{
|
| 434 |
-
"Symbol": t["symbol"], "Direction": t["direction"],
|
| 435 |
-
"
|
|
|
|
| 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"],
|
| 439 |
-
"
|
| 440 |
-
"Regime
|
| 441 |
-
|
| 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']}",
|
| 451 |
-
f"**
|
|
|
|
|
|
|
| 452 |
f"**Avg win / loss:** {m['avg_win']:.6f} / {m['avg_loss']:.6f}",
|
| 453 |
-
f"**Max DD:** {m['max_drawdown']:.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 —
|
| 465 |
-
gr.Markdown("# RCEA Lite v1.0 —
|
| 466 |
-
"HMM
|
| 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(
|
| 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("🪙
|
| 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:**
|
| 487 |
-
"Net of 0.
|
| 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__":
|