GoshawkVortexAI's picture
Update app.py
f4db522 verified
import gradio as gr
import requests
import pandas as pd
import numpy as np
from datetime import datetime
import time
# ──────────────────────────────────────────────────────────
# COIN LİSTESİ (CoinGecko ID → Sembol)
# ──────────────────────────────────────────────────────────
COINS = {
"bitcoin": "BTC",
"ethereum": "ETH",
"binancecoin": "BNB",
"ripple": "XRP",
"solana": "SOL",
"cardano": "ADA",
"dogecoin": "DOGE",
"avalanche-2": "AVAX",
"chainlink": "LINK",
"polkadot": "DOT",
"matic-network": "MATIC",
"tron": "TRX",
"litecoin": "LTC",
"uniswap": "UNI",
"cosmos": "ATOM",
"near": "NEAR",
"algorand": "ALGO",
"stellar": "XLM",
"filecoin": "FIL",
"internet-computer": "ICP",
"aptos": "APT",
"arbitrum": "ARB",
"optimism": "OP",
"sui": "SUI",
"pepe": "PEPE",
"shiba-inu": "SHIB",
"the-sandbox": "SAND",
"decentraland": "MANA",
"aave": "AAVE",
"maker": "MKR",
}
BASE_URL = "https://api.coingecko.com/api/v3"
HEADERS = {"accept": "application/json"}
# ──────────────────────────────────────────────────────────
# VERİ ÇEKME
# ──────────────────────────────────────────────────────────
def fetch_ohlc(coin_id: str, days: int = 30) -> pd.Series | None:
"""CoinGecko OHLC endpoint – günlük kapanış fiyatları."""
url = f"{BASE_URL}/coins/{coin_id}/ohlc?vs_currency=usd&days={days}"
try:
r = requests.get(url, headers=HEADERS, timeout=20)
r.raise_for_status()
data = r.json()
if not data:
return None
df = pd.DataFrame(data, columns=["ts", "open", "high", "low", "close"])
df["ts"] = pd.to_datetime(df["ts"], unit="ms")
df = df.set_index("ts").resample("D").last().dropna()
return df["close"]
except Exception:
return None
def fetch_market_chart(coin_id: str, days: int = 30) -> pd.Series | None:
"""Fallback: /market_chart ile günlük fiyat."""
url = f"{BASE_URL}/coins/{coin_id}/market_chart?vs_currency=usd&days={days}&interval=daily"
try:
r = requests.get(url, headers=HEADERS, timeout=20)
r.raise_for_status()
data = r.json().get("prices", [])
if not data:
return None
df = pd.DataFrame(data, columns=["ts", "price"])
df["ts"] = pd.to_datetime(df["ts"], unit="ms")
df = df.set_index("ts").resample("D").last().dropna()
return df["price"]
except Exception:
return None
def get_price_series(coin_id: str, days: int) -> pd.Series | None:
s = fetch_ohlc(coin_id, days)
if s is None or len(s) < 5:
s = fetch_market_chart(coin_id, days)
return s if s is not None and len(s) >= 5 else None
# ──────────────────────────────────────────────────────────
# HESAPLAMALAR
# ──────────────────────────────────────────────────────────
def returns(series: pd.Series) -> pd.Series:
return series.pct_change().dropna()
def rolling_corr(s1: pd.Series, s2: pd.Series, window: int = 14) -> float:
"""Son N günlük rolling correlation."""
r1 = returns(s1)
r2 = returns(s2)
combined = pd.concat([r1, r2], axis=1).dropna()
if len(combined) < window:
return float(combined.iloc[:, 0].corr(combined.iloc[:, 1]))
tail = combined.tail(window)
return float(tail.iloc[:, 0].corr(tail.iloc[:, 1]))
def zscore_spread(s1: pd.Series, s2: pd.Series, window: int = 20) -> dict | None:
"""
Pairs trading Z-score.
ratio = log(s1) - log(s2)
z = (ratio - mean) / std (rolling window)
"""
log1 = np.log(s1)
log2 = np.log(s2)
ratio = (log1 - log2).dropna()
if len(ratio) < window:
window = max(5, len(ratio) // 2)
roll_mean = ratio.rolling(window).mean()
roll_std = ratio.rolling(window).std()
zscore = ((ratio - roll_mean) / roll_std).dropna()
if len(zscore) == 0:
return None
current_z = float(zscore.iloc[-1])
current_ratio = float(ratio.iloc[-1])
ratio_mean = float(ratio.mean())
ratio_std = float(ratio.std())
half_life = _half_life(ratio)
return {
"current_z": current_z,
"current_ratio": current_ratio,
"ratio_mean": ratio_mean,
"ratio_std": ratio_std,
"half_life": half_life,
}
def _half_life(spread: pd.Series) -> float:
"""Ornstein-Uhlenbeck half-life (OLS)."""
try:
lag = spread.shift(1).dropna()
delta = spread.diff().dropna()
combined = pd.concat([lag, delta], axis=1).dropna()
if len(combined) < 5:
return float("nan")
x = combined.iloc[:, 0].values
y = combined.iloc[:, 1].values
A = np.vstack([np.ones(len(x)), x]).T
result = np.linalg.lstsq(A, y, rcond=None)
b = result[0][1]
if b >= 0:
return float("nan")
return round(-np.log(2) / b, 1)
except Exception:
return float("nan")
def trade_signal(z: float, corr: float, half_life: float) -> tuple[str, str]:
"""Sinyal üret."""
if abs(z) < 0.5:
return "NÖTR", "#555577"
if corr < 0.5:
return "DÜŞÜK KOR.", "#443344"
hl_ok = not np.isnan(half_life) and 2 <= half_life <= 60
if abs(z) >= 2.5 and hl_ok:
strength = "GÜÇLÜ"
elif abs(z) >= 1.5:
strength = "ORTA"
else:
strength = "İZLE"
if z > 2.0:
return f"{strength} · 1.SAT / 2.AL", "red"
elif z < -2.0:
return f"{strength} · 1.AL / 2.SAT", "green"
elif z > 1.0:
return f"{strength} · 1.SAT / 2.AL", "orange"
else:
return f"{strength} · 1.AL / 2.SAT", "yellow"
# ──────────────────────────────────────────────────────────
# ANA TARAMA
# ──────────────────────────────────────────────────────────
def scan_correlations(
selected_coins: list,
days: int,
min_corr: float,
min_abs_z: float,
progress=gr.Progress(),
):
if not selected_coins or len(selected_coins) < 2:
return _err_html("En az 2 coin seçin!"), None, "Tarama yapılmadı."
coin_ids = [k for k, v in COINS.items() if v in selected_coins]
n = len(coin_ids)
# 1) Fiyat serilerini çek
price_data = {}
for i, cid in enumerate(coin_ids):
sym = COINS[cid]
progress((i + 1) / (n + 1), desc=f"{sym} fiyatları çekiliyor...")
s = get_price_series(cid, days)
if s is not None:
price_data[sym] = s
time.sleep(0.35)
available = list(price_data.keys())
if len(available) < 2:
return _err_html("Yeterli fiyat verisi alınamadı. Tekrar deneyin."), None, "Hata."
# 2) Tüm çiftleri analiz et
pairs = []
total_pairs = len(available) * (len(available) - 1) // 2
idx = 0
for i in range(len(available)):
for j in range(i + 1, len(available)):
idx += 1
progress(idx / max(total_pairs, 1), desc=f"{available[i]}-{available[j]} analiz...")
s1 = price_data[available[i]]
s2 = price_data[available[j]]
common = s1.index.intersection(s2.index)
if len(common) < 10:
continue
s1c = s1.loc[common]
s2c = s2.loc[common]
corr = rolling_corr(s1c, s2c, window=min(14, len(common) - 1))
if abs(corr) < min_corr:
continue
spread_info = zscore_spread(s1c, s2c, window=min(20, len(common) - 2))
if spread_info is None:
continue
z = spread_info["current_z"]
if abs(z) < min_abs_z:
continue
hl = spread_info["half_life"]
signal, _ = trade_signal(z, corr, hl)
pairs.append({
"Coin 1": available[i],
"Coin 2": available[j],
"Korelasyon": round(corr, 4),
"Z-Score": round(z, 3),
"Half-Life (gün)": round(hl, 1) if not np.isnan(hl) else "-",
"Ratio (log)": round(spread_info["current_ratio"], 5),
"Ratio Ort.": round(spread_info["ratio_mean"], 5),
"Sinyal": signal,
})
if not pairs:
return _empty_html(min_corr, min_abs_z), None, f"Uygun çift bulunamadı (corr≥{min_corr}, |z|≥{min_abs_z})."
df = (
pd.DataFrame(pairs)
.sort_values("Z-Score", key=abs, ascending=False)
.reset_index(drop=True)
)
html = _render_html(df)
log = _make_log(df, days, available)
return html, df, log
# ──────────────────────────────────────────────────────────
# HTML RENDER
# ──────────────────────────────────────────────────────────
def _signal_style(sig: str) -> str:
if "GÜÇLÜ" in sig and "SAT" in sig:
return "background:#ff6b6b22;color:#ff6b6b;border:1px solid #ff6b6b55"
if "GÜÇLÜ" in sig and "AL" in sig:
return "background:#00ff8822;color:#00ff88;border:1px solid #00ff8855"
if "ORTA" in sig and "SAT" in sig:
return "background:#ff9f4322;color:#ff9f43;border:1px solid #ff9f4355"
if "ORTA" in sig and "AL" in sig:
return "background:#ffd70022;color:#ffd700;border:1px solid #ffd70055"
if "İZLE" in sig:
return "background:#7b7bff22;color:#7b7bff;border:1px solid #7b7bff55"
return "color:#555;border:1px solid #333"
def _corr_bar(c: float) -> str:
pct = int(abs(c) * 100)
color = "#00d2ff" if c > 0 else "#ff6b9d"
return (
f"<div style='display:flex;align-items:center;gap:8px'>"
f"<div style='width:64px;height:5px;background:#1a1a3a;border-radius:3px'>"
f"<div style='width:{pct}%;height:100%;background:{color};border-radius:3px'></div></div>"
f"<span style='color:{color};font-size:13px;font-weight:700'>{c:+.3f}</span></div>"
)
def _z_badge(z: float) -> str:
az = abs(z)
if az >= 2.5:
c, sz = ("#ff4444" if z > 0 else "#00ff88"), "17px"
elif az >= 1.5:
c, sz = ("#ff9f43" if z > 0 else "#ffd700"), "15px"
else:
c, sz = "#7b7bff", "13px"
arrow = "▲" if z > 0 else "▼"
return f"<span style='color:{c};font-size:{sz};font-weight:800'>{arrow} {z:+.3f}</span>"
def _hl_badge(hl) -> str:
if hl == "-" or (isinstance(hl, float) and np.isnan(hl)):
return "<span style='color:#444'>—</span>"
hl = float(hl)
if 2 <= hl <= 60:
c = "#00ff88"
elif hl < 2:
c = "#ff6b6b"
else:
c = "#555"
return f"<span style='color:{c};font-family:monospace'>{hl:.1f} gün</span>"
def _render_html(df: pd.DataFrame) -> str:
rows = ""
for rank, (_, row) in enumerate(df.iterrows(), 1):
sig = row["Sinyal"]
sstyle = _signal_style(sig)
rows += f"""
<tr>
<td style='color:#555;font-size:11px;text-align:center'>{rank}</td>
<td>
<span style='color:#c0c0ff;font-weight:700;font-size:14px'>{row['Coin 1']}</span>
<span style='color:#2a2a5a;margin:0 8px;font-size:18px'>⟷</span>
<span style='color:#c0c0ff;font-weight:700;font-size:14px'>{row['Coin 2']}</span>
</td>
<td>{_corr_bar(row['Korelasyon'])}</td>
<td>{_z_badge(row['Z-Score'])}</td>
<td>{_hl_badge(row['Half-Life (gün)'])}</td>
<td>
<span style='display:inline-block;padding:5px 12px;border-radius:6px;
font-size:11px;font-weight:700;letter-spacing:.5px;white-space:nowrap;{sstyle}'>
{sig}
</span>
</td>
</tr>"""
strong = len(df[df["Sinyal"].str.contains("GÜÇLÜ", na=False)])
orta = len(df[df["Sinyal"].str.contains("ORTA", na=False)])
izle = len(df[df["Sinyal"].str.contains("İZLE", na=False)])
return f"""
<style>
@import url('https://fonts.googleapis.com/css2?family=Space+Mono&family=Syne:wght@400;700;800&display=swap');
.ct-wrap {{ background:#06060f;border-radius:14px;overflow:hidden;font-family:'Syne',sans-serif }}
.ct-hdr {{ background:linear-gradient(135deg,#0d0d2a,#12122a);padding:16px 22px;
display:flex;justify-content:space-between;align-items:center;
border-bottom:1px solid #1a1a3a }}
.ct-title {{ font-size:11px;font-weight:800;letter-spacing:3px;color:#7b7bff }}
.ct-stats {{ display:flex;gap:20px }}
.ct-sv {{ font-size:22px;font-weight:800;text-align:center }}
.ct-sl {{ font-size:9px;color:#444;letter-spacing:1.5px;text-align:center }}
.ct-tbl {{ width:100%;border-collapse:collapse }}
.ct-tbl th {{ background:#0d0d22;color:#5555aa;padding:9px 14px;
font-size:9px;letter-spacing:2px;text-transform:uppercase;
text-align:left;border-bottom:1px solid #1a1a3a }}
.ct-tbl td {{ padding:12px 14px;border-bottom:1px solid #0e0e22;vertical-align:middle }}
.ct-tbl tr:hover td {{ background:#090918 }}
.ct-tbl tr:last-child td {{ border-bottom:none }}
</style>
<div class='ct-wrap'>
<div class='ct-hdr'>
<span class='ct-title'>⚡ KORELASYon TİCARETİ &nbsp;·&nbsp; {datetime.now().strftime('%H:%M:%S')}</span>
<div class='ct-stats'>
<div><div class='ct-sv' style='color:#ff6b6b'>{strong}</div><div class='ct-sl'>GÜÇLÜ</div></div>
<div><div class='ct-sv' style='color:#ffd700'>{orta}</div><div class='ct-sl'>ORTA</div></div>
<div><div class='ct-sv' style='color:#7b7bff'>{izle}</div><div class='ct-sl'>İZLE</div></div>
<div><div class='ct-sv' style='color:#aaa'>{len(df)}</div><div class='ct-sl'>TOPLAM</div></div>
</div>
</div>
<table class='ct-tbl'>
<thead><tr>
<th>#</th><th>Çift</th><th>Korelasyon</th>
<th>Z-Score</th><th>Half-Life</th><th>Sinyal</th>
</tr></thead>
<tbody>{rows}</tbody>
</table>
</div>"""
def _err_html(msg: str) -> str:
return (
f"<div style='background:#1a0a0a;border:1px solid #ff4444;border-radius:10px;"
f"padding:30px;text-align:center;color:#ff6b6b;font-family:monospace'>⚠️ {msg}</div>"
)
def _empty_html(min_corr: float, min_abs_z: float) -> str:
return (
f"<div style='background:#06060f;border:1px solid #1a1a3a;border-radius:12px;"
f"padding:50px;text-align:center;font-family:monospace'>"
f"<div style='font-size:40px'>📭</div>"
f"<div style='color:#7b7bff;margin-top:14px'>corr≥{min_corr} ve |Z|≥{min_abs_z} koşullarını sağlayan çift bulunamadı.</div>"
f"<div style='color:#444;margin-top:8px;font-size:12px'>Eşikleri düşürün veya daha fazla coin seçin.</div></div>"
)
def _make_log(df: pd.DataFrame, days: int, available: list) -> str:
lines = [
f"[{datetime.now().strftime('%H:%M:%S')}] Tarama tamamlandı",
f" Analiz edilen coinler : {', '.join(available)}",
f" Tarihsel veri süresi : {days} gün",
f" Bulunan çift sayısı : {len(df)}",
"─" * 60,
]
for _, row in df.iterrows():
lines.append(
f" {row['Coin 1']:<6}{row['Coin 2']:<6} "
f"corr={row['Korelasyon']:+.3f} z={row['Z-Score']:+.3f} "
f"HL={row['Half-Life (gün)']} gün → {row['Sinyal']}"
)
return "\n".join(lines)
# ──────────────────────────────────────────────────────────
# GRADIO ARAYÜZÜ
# ──────────────────────────────────────────────────────────
ALL_SYMBOLS = sorted(COINS.values())
DEFAULT_COINS = ["BTC", "ETH", "BNB", "SOL", "AVAX", "LINK", "ADA", "MATIC", "DOT", "ATOM"]
CSS = """
@import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;700;800&display=swap');
body, .gradio-container {
background:#06060f !important;
color:#e0e0ff !important;
font-family:'Syne',sans-serif !important;
}
.gr-button { font-family:'Syne',sans-serif !important; font-weight:700 !important; }
.gr-button-primary {
background:linear-gradient(135deg,#7b7bff,#00d2ff) !important;
color:#000 !important; border:none !important; letter-spacing:1px !important;
}
.gr-button-secondary {
background:#0d0d22 !important;
color:#7b7bff !important;
border:1px solid #2a2a4a !important;
}
label { color:#aaa !important; font-size:11px !important; letter-spacing:1px !important; }
.gr-panel, .gr-box { background:#0a0a1a !important; border:1px solid #1a1a3a !important; }
footer { display:none !important; }
"""
HEADER_HTML = """
<div style='text-align:center;padding:28px 0 12px;font-family:Syne,sans-serif'>
<div style='
font-size:26px;font-weight:800;letter-spacing:5px;
background:linear-gradient(90deg,#7b7bff,#00d2ff,#00ff88);
-webkit-background-clip:text;-webkit-text-fill-color:transparent
'>KORELASYON TRADİNG SKANER</div>
<div style='color:#333;font-size:11px;letter-spacing:3px;margin-top:5px'>
PAIRS TRADING · Z-SCORE · MEAN REVERSION · COINGECKO · API ANAHTARI YOK
</div>
</div>"""
INFO_HTML = """
<div style='background:#0a0a1e;border:1px solid #1a1a3a;border-radius:10px;
padding:14px 16px;font-size:12px;color:#666;line-height:2;margin-top:4px'>
<div style='color:#7b7bff;font-weight:700;letter-spacing:1px;margin-bottom:4px'>📐 ALGORİTMA</div>
<b style='color:#aaa'>Z-Score</b> = (log_ratio − ort) / std<br>
<b style='color:#aaa'>Half-Life</b> = OU modeli ile mean-reversion hızı<br>
<span style='color:#00ff88'>|Z| &gt; 2</span> → İşlem sinyali<br>
<span style='color:#ffd700'>2 – 60 gün</span> → İdeal half-life aralığı<br>
<span style='color:#ff6b6b'>Korelasyon &gt; 0.7</span> → Güvenilir çift
</div>"""
def export_csv(df):
if df is None:
return None
path = "/tmp/korelasyon_sonuclari.csv"
df.to_csv(path, index=False, encoding="utf-8-sig")
return path
with gr.Blocks(title="Korelasyon Trading Skaner") as demo:
gr.HTML(HEADER_HTML)
with gr.Row():
# ── Sol panel: coin seçimi ──
with gr.Column(scale=1, min_width=200):
coin_selector = gr.CheckboxGroup(
choices=ALL_SYMBOLS,
value=DEFAULT_COINS,
label="COİN SEÇİMİ",
)
gr.HTML(INFO_HTML)
# ── Sağ panel: kontroller + sonuç ──
with gr.Column(scale=3):
with gr.Row():
days_slider = gr.Slider(
minimum=14, maximum=90, value=30, step=7,
label="VERİ SÜRESİ (GÜN)",
)
corr_slider = gr.Slider(
minimum=0.3, maximum=0.99, value=0.65, step=0.05,
label="MİN. KORELASYON",
)
z_slider = gr.Slider(
minimum=0.5, maximum=4.0, value=1.5, step=0.25,
label="MİN. |Z-SCORE|",
)
with gr.Row():
scan_btn = gr.Button("⚡ TARAMAYI BAŞLAT", variant="primary", size="lg")
clear_btn = gr.Button("🗑 TEMİZLE", variant="secondary")
export_btn = gr.Button("📥 CSV İNDİR", variant="secondary")
result_html = gr.HTML(
value=(
"<div style='color:#222;text-align:center;padding:70px;"
"font-family:monospace;font-size:13px'>Coin seçip taramayı başlatın →</div>"
)
)
with gr.Accordion("📋 LOG / DETAY", open=False):
log_box = gr.Textbox(
label="",
lines=14,
max_lines=22,
placeholder="Tarama logları burada görünür...",
)
df_state = gr.State(None)
csv_file = gr.File(label="CSV Dosyası", visible=False)
# ── Bağlantılar ──
scan_btn.click(
fn=scan_correlations,
inputs=[coin_selector, days_slider, corr_slider, z_slider],
outputs=[result_html, df_state, log_box],
)
clear_btn.click(
fn=lambda: (
"<div style='color:#222;text-align:center;padding:70px;font-family:monospace'>Temizlendi.</div>",
None,
"",
),
outputs=[result_html, df_state, log_box],
)
export_btn.click(
fn=export_csv,
inputs=[df_state],
outputs=[csv_file],
).then(fn=lambda: gr.update(visible=True), outputs=[csv_file])
if __name__ == "__main__":
demo.launch(css=CSS)