GoshawkVortexAI commited on
Commit
2f09aa8
·
verified ·
1 Parent(s): 5437a9b

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +189 -0
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)