Spaces:
Sleeping
Sleeping
Upload app.py
Browse files
app.py
CHANGED
|
@@ -2972,6 +2972,172 @@ def load_stat_arb(sym_a, sym_b, bar, n_bars, hw, zw, entry, exit_, stop):
|
|
| 2972 |
return error_box(f"Stat-Arb hesaplanamadı: {e}"), _empty_fig("Hata", "Stat-Arb")
|
| 2973 |
|
| 2974 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2975 |
def tv_symbol(symbol):
|
| 2976 |
"""OKX 'BTC-USDT' -> TradingView 'OKX:BTCUSDT'."""
|
| 2977 |
return "OKX:" + str(symbol or "BTC-USDT").replace("-", "").upper()
|
|
@@ -3046,6 +3212,23 @@ with gr.Blocks(title="OKX Quant Terminal") as demo:
|
|
| 3046 |
tv_html = gr.HTML()
|
| 3047 |
with gr.Tab("⚖️ İstatistiksel Arbitraj"):
|
| 3048 |
gr.Markdown("İki varlık arasında eşbütünleşme / spread z-skor tabanlı pairs trading + komisyonlu backtest.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3049 |
with gr.Row():
|
| 3050 |
sa_a = gr.Dropdown(choices=POPULAR_SYMBOLS, value="BTC-USDT", allow_custom_value=True, label="Varlık A", scale=2)
|
| 3051 |
sa_b = gr.Dropdown(choices=POPULAR_SYMBOLS, value="ETH-USDT", allow_custom_value=True, label="Varlık B", scale=2)
|
|
@@ -3091,6 +3274,12 @@ with gr.Blocks(title="OKX Quant Terminal") as demo:
|
|
| 3091 |
risk_sl.release(refresh_signal, sig_inputs, sig_outputs)
|
| 3092 |
scan_btn.click(run_scanner, [scan_symbols, scan_bar, closed_cb, scan_mode], scan_out)
|
| 3093 |
sa_btn.click(load_stat_arb, [sa_a, sa_b, sa_bar, sa_n, sa_hw, sa_zw, sa_en, sa_ex, sa_st], [sa_html, sa_plot])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3094 |
demo.load(refresh_all, io_inputs, io_outputs)
|
| 3095 |
# TradingView sekmesi yalnızca sembol / zaman dilimi değişince yenilenir (15 sn'lik otomatik yenilemede iframe sıfırlanmasın)
|
| 3096 |
symbol_dd.change(tv_widget_html, [symbol_dd, bar_dd], tv_html)
|
|
|
|
| 2972 |
return error_box(f"Stat-Arb hesaplanamadı: {e}"), _empty_fig("Hata", "Stat-Arb")
|
| 2973 |
|
| 2974 |
|
| 2975 |
+
# ---------------------------------------------------------------------------
|
| 2976 |
+
# OTOMATİK STAT-ARB TARAYICI (tüm likit coinler, tüm çiftler)
|
| 2977 |
+
# API/CPU verimliliği:
|
| 2978 |
+
# 1) Her coin yalnızca 1 kez çekilir (N coin = N istek dizisi; N² çift için ayrı istek YOK).
|
| 2979 |
+
# 2) Sonraki taramalarda yalnızca eksik son mumlar çekilir (tek istek) ve önbelleğe eklenir.
|
| 2980 |
+
# 3) Çiftler hunisi: getiri korelasyonu (tek matris işlemi) → EG-ADF (iki yön) → half-life → OOS teyidi
|
| 2981 |
+
# → yalnızca ilk SA_TOP_K çifte tam backtest.
|
| 2982 |
+
# ---------------------------------------------------------------------------
|
| 2983 |
+
SA_CACHE_TTL = 600 # sn — bu süre içinde yeniden tarama ağa hiç çıkmaz
|
| 2984 |
+
SA_TOP_K = 15 # tam backtest yapılacak en iyi çift sayısı
|
| 2985 |
+
SA_OOS_CRIT = -2.86 # sabit β ile test edilen OOS spread için standart ADF %5 kritik değeri
|
| 2986 |
+
SA_EXCLUDE = {"USDC", "DAI", "TUSD", "FDUSD", "USDD", "PYUSD", "USDG", "EURT", "USDT"} # stabil / sabitlenmiş
|
| 2987 |
+
_SA_STORE, _SA_LOCK = {}, threading.Lock()
|
| 2988 |
+
|
| 2989 |
+
|
| 2990 |
+
def _sa_clean(df):
|
| 2991 |
+
if "confirm" in df.columns: # oluşmakta olan (kapanmamış) mumu at
|
| 2992 |
+
df = df[df["confirm"].astype(str) == "1"]
|
| 2993 |
+
return df[["ts", "c"]].drop_duplicates("ts").sort_values("ts").reset_index(drop=True)
|
| 2994 |
+
|
| 2995 |
+
|
| 2996 |
+
def _sa_closes(sym, bar, n):
|
| 2997 |
+
"""Coin başına kapanışlar: ilk seferde sayfalayarak, sonra yalnızca eksik son mumlar (1 istek)."""
|
| 2998 |
+
now = time.time()
|
| 2999 |
+
with _SA_LOCK:
|
| 3000 |
+
hit = _SA_STORE.get((sym, bar))
|
| 3001 |
+
if hit and len(hit[1]) >= n * 0.95:
|
| 3002 |
+
if now - hit[0] < SA_CACHE_TTL:
|
| 3003 |
+
return hit[1].tail(n)
|
| 3004 |
+
old = hit[1]
|
| 3005 |
+
step = old["ts"].diff().median()
|
| 3006 |
+
try:
|
| 3007 |
+
new = _sa_clean(fetch_candles(sym, bar, 100))
|
| 3008 |
+
if len(new) and new["ts"].min() <= old["ts"].iloc[-1] + step: # boşluk yok → birleştir
|
| 3009 |
+
df = (pd.concat([old, new]).drop_duplicates("ts", keep="last")
|
| 3010 |
+
.sort_values("ts").tail(n + 50).reset_index(drop=True))
|
| 3011 |
+
with _SA_LOCK:
|
| 3012 |
+
_SA_STORE[(sym, bar)] = (now, df)
|
| 3013 |
+
return df.tail(n)
|
| 3014 |
+
except Exception:
|
| 3015 |
+
pass
|
| 3016 |
+
df = _sa_clean(fetch_candles_long(sym, bar, n))
|
| 3017 |
+
with _SA_LOCK:
|
| 3018 |
+
_SA_STORE[(sym, bar)] = (now, df)
|
| 3019 |
+
return df.tail(n)
|
| 3020 |
+
|
| 3021 |
+
|
| 3022 |
+
def _sa_universe(top_n):
|
| 3023 |
+
"""OKX spot'ta 24s USDT hacmine göre en likit top_n coin (stabiller hariç)."""
|
| 3024 |
+
try:
|
| 3025 |
+
tk = fetch_all_tickers("SPOT")
|
| 3026 |
+
except Exception:
|
| 3027 |
+
return list(SCAN_DEFAULT)[:top_n]
|
| 3028 |
+
rows = []
|
| 3029 |
+
for t in tk:
|
| 3030 |
+
sid = t.get("instId", "")
|
| 3031 |
+
if not sid.endswith("-USDT") or sid.split("-")[0] in SA_EXCLUDE:
|
| 3032 |
+
continue
|
| 3033 |
+
try:
|
| 3034 |
+
rows.append((float(t.get("volCcy24h") or 0), sid))
|
| 3035 |
+
except (TypeError, ValueError):
|
| 3036 |
+
continue
|
| 3037 |
+
rows.sort(reverse=True)
|
| 3038 |
+
return [s for _, s in rows[:top_n]]
|
| 3039 |
+
|
| 3040 |
+
|
| 3041 |
+
def sa_find_pairs(px, min_corr=0.5, max_hl=48, crit=-3.34, hl_min=1.5):
|
| 3042 |
+
"""px: kapanış DataFrame'i (sütun = sembol). Hunide elenen çiftlerin EG/OOS özetlerini döndürür (sıralı)."""
|
| 3043 |
+
L = np.log(px.values.astype(float))
|
| 3044 |
+
T, N = L.shape
|
| 3045 |
+
syms = list(px.columns)
|
| 3046 |
+
C = np.corrcoef(np.diff(L, axis=0).T)
|
| 3047 |
+
iu, ju = np.triu_indices(N, 1)
|
| 3048 |
+
cv = C[iu, ju]
|
| 3049 |
+
keep = np.where(cv >= min_corr)[0]
|
| 3050 |
+
keep = keep[np.argsort(-cv[keep])][:3000]
|
| 3051 |
+
h = T // 2
|
| 3052 |
+
found = []
|
| 3053 |
+
for k in keep:
|
| 3054 |
+
i, j = int(iu[k]), int(ju[k])
|
| 3055 |
+
best = None
|
| 3056 |
+
for yi, xi in ((i, j), (j, i)): # EG asimetriktir: iki yönü dene, güçlü olanı al
|
| 3057 |
+
y, x = L[:, yi], L[:, xi]
|
| 3058 |
+
b, a = np.polyfit(x, y, 1)
|
| 3059 |
+
t, g = _adf_tstat(y - a - b * x)
|
| 3060 |
+
if best is None or t < best[0]:
|
| 3061 |
+
best = (t, g, b, a, yi, xi)
|
| 3062 |
+
t, g, b, a, yi, xi = best
|
| 3063 |
+
if t > crit or not (-1 < g < 0):
|
| 3064 |
+
continue
|
| 3065 |
+
hl = -math.log(2) / math.log(1 + g)
|
| 3066 |
+
if not (hl_min <= hl <= max_hl):
|
| 3067 |
+
continue
|
| 3068 |
+
y, x = L[:, yi], L[:, xi]
|
| 3069 |
+
b1, a1 = np.polyfit(x[:h], y[:h], 1) # 1. yarıda β → 2. yarıda spread kalıcılığı (OOS)
|
| 3070 |
+
t_oos, _ = _adf_tstat(y[h:] - a1 - b1 * x[h:])
|
| 3071 |
+
b2 = np.polyfit(x[h:], y[h:], 1)[0]
|
| 3072 |
+
found.append(dict(yi=yi, xi=xi, t=float(t), t_oos=float(t_oos), hl=float(hl), corr=float(cv[k]),
|
| 3073 |
+
beta=float(b), drift=float(abs(b2 - b1) / abs(b1)) if b1 else float("nan")))
|
| 3074 |
+
found.sort(key=lambda d: (d["t_oos"] > SA_OOS_CRIT, d["t"])) # önce OOS'u geçenler, sonra en negatif t
|
| 3075 |
+
return found, syms, len(keep)
|
| 3076 |
+
|
| 3077 |
+
|
| 3078 |
+
def run_sa_scan(bar, n_bars, top_n, min_corr, max_hl, crit, entry, progress=gr.Progress()):
|
| 3079 |
+
try:
|
| 3080 |
+
n_bars, top_n = int(n_bars), int(top_n)
|
| 3081 |
+
syms = _sa_universe(top_n)
|
| 3082 |
+
closes, done = {}, 0
|
| 3083 |
+
with ThreadPoolExecutor(max_workers=4) as ex: # OKX limiti için 4 paralel
|
| 3084 |
+
futs = {ex.submit(_sa_closes, s, bar, n_bars): s for s in syms}
|
| 3085 |
+
for f in as_completed(futs):
|
| 3086 |
+
done += 1
|
| 3087 |
+
progress(0.85 * done / len(syms), desc=f"{futs[f]} ({done}/{len(syms)})")
|
| 3088 |
+
try:
|
| 3089 |
+
d = f.result()
|
| 3090 |
+
if len(d) >= n_bars * 0.9:
|
| 3091 |
+
closes[futs[f]] = d.set_index("ts")["c"]
|
| 3092 |
+
except Exception:
|
| 3093 |
+
continue
|
| 3094 |
+
if len(closes) < 3:
|
| 3095 |
+
return pd.DataFrame(), error_box("Yeterli coin verisi alınamadı (ağ / rate-limit). Biraz sonra tekrar deneyin.")
|
| 3096 |
+
px = pd.concat(closes, axis=1).sort_index().tail(n_bars)
|
| 3097 |
+
px = px.dropna(axis=1, thresh=int(len(px) * 0.9)).dropna(how="any") # kısa geçmişli coinleri ele, ortak barlar
|
| 3098 |
+
if len(px) < 250 or px.shape[1] < 3:
|
| 3099 |
+
return pd.DataFrame(), error_box(f"Ortak veri yetersiz ({len(px)} bar, {px.shape[1]} coin). Bar sayısını artırın.")
|
| 3100 |
+
progress(0.9, desc="Çiftler test ediliyor…")
|
| 3101 |
+
found, cols, n_tested = sa_find_pairs(px, float(min_corr), float(max_hl), float(crit))
|
| 3102 |
+
rows = []
|
| 3103 |
+
for d in found[:SA_TOP_K]:
|
| 3104 |
+
sa_, sb_ = cols[d["yi"]], cols[d["xi"]]
|
| 3105 |
+
bt = pairs_backtest(px[sa_], px[sb_], 150, 48, float(entry), 0.5, 3.5)
|
| 3106 |
+
zc, pc = float(bt["z"].iloc[-1]), int(bt["pos"].iloc[-1])
|
| 3107 |
+
sig = ("LONG spread (A al / B sat)" if pc == 1 else "SHORT spread (A sat / B al)" if pc == -1 else
|
| 3108 |
+
"Giriş bölgesinde" if abs(zc) >= float(entry) else "Bekle")
|
| 3109 |
+
rows.append({"A": sa_, "B": sb_, "EG t": round(d["t"], 2), "OOS t": round(d["t_oos"], 2),
|
| 3110 |
+
"OOS": "✓" if d["t_oos"] <= SA_OOS_CRIT else "✗",
|
| 3111 |
+
"Half-life (bar)": round(d["hl"], 1), "Korelasyon": round(d["corr"], 2),
|
| 3112 |
+
"β": round(d["beta"], 2), "β kayması %": round(d["drift"] * 100, 0),
|
| 3113 |
+
"Z": round(zc, 2), "Durum": sig, "İşlem": len(bt["trades"]),
|
| 3114 |
+
"Net getiri %": round(float(bt["eq"].iloc[-1] - 1) * 100, 2)})
|
| 3115 |
+
n_oos = sum(1 for d in found if d["t_oos"] <= SA_OOS_CRIT)
|
| 3116 |
+
exp_fp = 0.05 * n_tested
|
| 3117 |
+
info = (section(f"OTOMATİK STAT-ARB TARAMA · {bar} · {px.shape[1]} coin · {len(px)} bar")
|
| 3118 |
+
+ f'<div class="mc-sub">{n_tested} çift korelasyon süzgecini geçti ve test edildi → {len(found)} çift EG/half-life eşiğini geçti, '
|
| 3119 |
+
f'{n_oos} tanesi OOS teyidini de geçti. <b>Çoklu test uyarısı:</b> %5 eşikte ~{exp_fp:.0f} yanlış pozitif beklenir; '
|
| 3120 |
+
f'“OOS ✓” ve makul half-life olmayanlara güvenmeyin. İlk {SA_TOP_K} çift gösteriliyor. '
|
| 3121 |
+
'Satıra tıklayınca A/B seçilir ve aşağıda tam analiz + backtest açılır.</div>')
|
| 3122 |
+
if not rows:
|
| 3123 |
+
return pd.DataFrame(), info + error_box("Kriterleri geçen çift yok. Bu normaldir; eşikleri gevşetmek sahte sinyal sayısını artırır.")
|
| 3124 |
+
return pd.DataFrame(rows), info
|
| 3125 |
+
except Exception as e:
|
| 3126 |
+
return pd.DataFrame(), error_box(f"Tarama hatası: {e}")
|
| 3127 |
+
|
| 3128 |
+
|
| 3129 |
+
def sa_pick_pair(df, bar, evt: gr.SelectData):
|
| 3130 |
+
"""Taramada satıra tıklanınca A/B/zaman dilimini manuel analiz alanlarına yazar."""
|
| 3131 |
+
try:
|
| 3132 |
+
rv = getattr(evt, "row_value", None)
|
| 3133 |
+
if rv:
|
| 3134 |
+
return rv[0], rv[1], bar
|
| 3135 |
+
r = evt.index[0] if isinstance(evt.index, (list, tuple)) else evt.index
|
| 3136 |
+
return df.iloc[r]["A"], df.iloc[r]["B"], bar
|
| 3137 |
+
except Exception:
|
| 3138 |
+
return gr.update(), gr.update(), gr.update()
|
| 3139 |
+
|
| 3140 |
+
|
| 3141 |
def tv_symbol(symbol):
|
| 3142 |
"""OKX 'BTC-USDT' -> TradingView 'OKX:BTCUSDT'."""
|
| 3143 |
return "OKX:" + str(symbol or "BTC-USDT").replace("-", "").upper()
|
|
|
|
| 3212 |
tv_html = gr.HTML()
|
| 3213 |
with gr.Tab("⚖️ İstatistiksel Arbitraj"):
|
| 3214 |
gr.Markdown("İki varlık arasında eşbütünleşme / spread z-skor tabanlı pairs trading + komisyonlu backtest.")
|
| 3215 |
+
gr.Markdown("**🔎 Otomatik tarama:** OKX spot'ta hacme göre en likit coinlerin TÜM çiftlerini eşbütünleşme huni testinden geçirir "
|
| 3216 |
+
"(her coin 1 kez çekilir, tekrar taramada yalnızca yeni mumlar). Satıra tıkla → analiz otomatik açılır.")
|
| 3217 |
+
with gr.Row():
|
| 3218 |
+
sas_bar = gr.Dropdown(choices=BAR_OPTIONS, value="1H", label="Zaman Dilimi", scale=1)
|
| 3219 |
+
sas_n = gr.Slider(300, 2000, value=600, step=100, label="Bar sayısı", scale=1)
|
| 3220 |
+
sas_top = gr.Slider(10, 150, value=60, step=5, label="Coin sayısı (hacme göre)", scale=1)
|
| 3221 |
+
with gr.Row():
|
| 3222 |
+
sas_corr = gr.Slider(0.3, 0.95, value=0.5, step=0.05, label="Min getiri korelasyonu", scale=1)
|
| 3223 |
+
sas_hl = gr.Slider(5, 200, value=48, step=1, label="Maks half-life (bar)", scale=1)
|
| 3224 |
+
sas_crit = gr.Slider(-4.5, -2.9, value=-3.34, step=0.02, label="EG eşiği (t)", scale=1)
|
| 3225 |
+
with gr.Row():
|
| 3226 |
+
sas_btn = gr.Button("🔎 Tüm Coinleri Tara", variant="primary", scale=2)
|
| 3227 |
+
sas_auto = gr.Checkbox(label="Otomatik tara (15 dk)", value=False, scale=1)
|
| 3228 |
+
sas_timer = gr.Timer(900, active=False)
|
| 3229 |
+
sas_html = gr.HTML()
|
| 3230 |
+
sas_out = gr.Dataframe(interactive=False, wrap=True)
|
| 3231 |
+
gr.Markdown("---\n**Manuel çift analizi**")
|
| 3232 |
with gr.Row():
|
| 3233 |
sa_a = gr.Dropdown(choices=POPULAR_SYMBOLS, value="BTC-USDT", allow_custom_value=True, label="Varlık A", scale=2)
|
| 3234 |
sa_b = gr.Dropdown(choices=POPULAR_SYMBOLS, value="ETH-USDT", allow_custom_value=True, label="Varlık B", scale=2)
|
|
|
|
| 3274 |
risk_sl.release(refresh_signal, sig_inputs, sig_outputs)
|
| 3275 |
scan_btn.click(run_scanner, [scan_symbols, scan_bar, closed_cb, scan_mode], scan_out)
|
| 3276 |
sa_btn.click(load_stat_arb, [sa_a, sa_b, sa_bar, sa_n, sa_hw, sa_zw, sa_en, sa_ex, sa_st], [sa_html, sa_plot])
|
| 3277 |
+
sas_in = [sas_bar, sas_n, sas_top, sas_corr, sas_hl, sas_crit, sa_en]
|
| 3278 |
+
sas_btn.click(run_sa_scan, sas_in, [sas_out, sas_html])
|
| 3279 |
+
sas_timer.tick(run_sa_scan, sas_in, [sas_out, sas_html])
|
| 3280 |
+
sas_auto.change(lambda active: gr.Timer(active=active), sas_auto, sas_timer)
|
| 3281 |
+
sas_out.select(sa_pick_pair, [sas_out, sas_bar], [sa_a, sa_b, sa_bar]).then(
|
| 3282 |
+
load_stat_arb, [sa_a, sa_b, sa_bar, sa_n, sa_hw, sa_zw, sa_en, sa_ex, sa_st], [sa_html, sa_plot])
|
| 3283 |
demo.load(refresh_all, io_inputs, io_outputs)
|
| 3284 |
# TradingView sekmesi yalnızca sembol / zaman dilimi değişince yenilenir (15 sn'lik otomatik yenilemede iframe sıfırlanmasın)
|
| 3285 |
symbol_dd.change(tv_widget_html, [symbol_dd, bar_dd], tv_html)
|