GoshawkVortexAI commited on
Commit
2bae030
·
verified ·
1 Parent(s): e5f9c85

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +510 -389
app.py CHANGED
@@ -1,444 +1,565 @@
1
- """
2
- Kripto Arbitraj Tarayıcı - Gradio Uygulaması
3
- Borsa fiyat farklarını tespit eder ve arbitraj fırsatlarını gösterir.
4
- """
5
-
6
  import gradio as gr
7
  import requests
8
  import pandas as pd
9
- import time
10
  from datetime import datetime
 
11
 
12
- # ─────────────────────────────────────────────
13
- # BORSA API FONKSİYONLARI
14
- # ─────────────────────────────────────────────
15
-
16
- BORSALAR = {
17
- "Binance": "https://api.binance.com/api/v3/ticker/price?symbol={symbol}USDT",
18
- "KuCoin": "https://api.kucoin.com/api/v1/market/orderbook/level1?symbol={symbol}-USDT",
19
- "Bybit": "https://api.bybit.com/v5/market/tickers?category=spot&symbol={symbol}USDT",
20
- "OKX": "https://www.okx.com/api/v5/market/ticker?instId={symbol}-USDT",
21
- "Gate.io": "https://api.gateio.ws/api/v4/spot/tickers?currency_pair={symbol}_USDT",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  }
23
 
24
- DESTEKLENEN_COINLER = [
25
- "BTC", "ETH", "BNB", "SOL", "XRP",
26
- "ADA", "AVAX", "DOT", "MATIC", "LINK",
27
- "LTC", "UNI", "ATOM", "NEAR", "APT",
28
- ]
29
 
 
 
 
30
 
31
- def binance_fiyat(symbol: str) -> float | None:
 
 
32
  try:
33
- r = requests.get(
34
- f"https://api.binance.com/api/v3/ticker/price?symbol={symbol}USDT",
35
- timeout=5
36
- )
37
- return float(r.json()["price"])
 
 
 
 
38
  except Exception:
39
  return None
40
 
41
 
42
- def kucoin_fiyat(symbol: str) -> float | None:
 
 
43
  try:
44
- r = requests.get(
45
- f"https://api.kucoin.com/api/v1/market/orderbook/level1?symbol={symbol}-USDT",
46
- timeout=5
47
- )
48
- data = r.json()
49
- price = data.get("data", {}).get("price")
50
- return float(price) if price else None
 
 
51
  except Exception:
52
  return None
53
 
54
 
55
- def bybit_fiyat(symbol: str) -> float | None:
56
- try:
57
- r = requests.get(
58
- f"https://api.bybit.com/v5/market/tickers?category=spot&symbol={symbol}USDT",
59
- timeout=5
60
- )
61
- items = r.json().get("result", {}).get("list", [])
62
- if items:
63
- return float(items[0]["lastPrice"])
64
- return None
65
- except Exception:
66
- return None
67
 
68
 
69
- def okx_fiyat(symbol: str) -> float | None:
70
- try:
71
- r = requests.get(
72
- f"https://www.okx.com/api/v5/market/ticker?instId={symbol}-USDT",
73
- timeout=5
74
- )
75
- data = r.json().get("data", [])
76
- if data:
77
- return float(data[0]["last"])
78
- return None
79
- except Exception:
80
- return None
81
 
 
 
82
 
83
- def gateio_fiyat(symbol: str) -> float | None:
84
- try:
85
- r = requests.get(
86
- f"https://api.gateio.ws/api/v4/spot/tickers?currency_pair={symbol}_USDT",
87
- timeout=5
88
- )
89
- data = r.json()
90
- if data:
91
- return float(data[0]["last"])
92
- return None
93
- except Exception:
94
- return None
95
 
 
 
 
 
 
 
 
 
 
96
 
97
- BORSA_FONKSIYONLARI = {
98
- "Binance": binance_fiyat,
99
- "KuCoin": kucoin_fiyat,
100
- "Bybit": bybit_fiyat,
101
- "OKX": okx_fiyat,
102
- "Gate.io": gateio_fiyat,
103
- }
104
 
105
- # ─────────────────────────────────────────────
106
- # ARBİTRAJ HESAPLAMA
107
- # ─────────────────────────────────────────────
108
-
109
- def fiyatlari_cek(coinler: list[str], borsalar: list[str]) -> dict:
110
- """Seçili coin ve borsalar için fiyatları çeker."""
111
- sonuclar = {}
112
- for coin in coinler:
113
- sonuclar[coin] = {}
114
- for borsa in borsalar:
115
- fn = BORSA_FONKSIYONLARI.get(borsa)
116
- if fn:
117
- sonuclar[coin][borsa] = fn(coin)
118
- return sonuclar
119
-
120
-
121
- def arbitraj_hesapla(
122
- secili_coinler: list[str],
123
- secili_borsalar: list[str],
124
- min_fark: float,
125
- islem_ucreti: float,
126
- ) -> tuple[str, str, str]:
127
  """
128
- Arbitraj fırsatlarını hesaplar.
129
- Döndürür: (durum_mesaji, fiyat_tablosu_html, firsat_tablosu_html)
 
130
  """
131
- if not secili_coinler:
132
- return "⚠️ En az bir coin seçin.", "", ""
133
- if len(secili_borsalar) < 2:
134
- return "⚠️ En az iki borsa seçin.", "", ""
135
-
136
- zaman = datetime.now().strftime("%H:%M:%S")
137
- durum = f"🔄 Fiyatlar çekiliyor... ({zaman})"
138
-
139
- fiyatlar = fiyatlari_cek(secili_coinler, secili_borsalar)
140
-
141
- # ── Fiyat tablosu ──────────────────────────────
142
- fiyat_satirlari = ""
143
- for coin, borsa_fiyatlari in fiyatlar.items():
144
- for borsa, fiyat in borsa_fiyatlari.items():
145
- gosterim = f"${fiyat:,.4f}" if fiyat else "—"
146
- fiyat_satirlari += f"<tr><td>{coin}</td><td>{borsa}</td><td>{gosterim}</td></tr>\n"
147
-
148
- fiyat_tablosu = f"""
149
- <div class="tablo-kap">
150
- <h3 class="tablo-baslik">📊 Anlık Fiyatlar</h3>
151
- <table class="arb-tablo">
152
- <thead><tr><th>Coin</th><th>Borsa</th><th>Fiyat (USDT)</th></tr></thead>
153
- <tbody>{fiyat_satirlari}</tbody>
154
- </table>
155
- </div>
156
- """
157
-
158
- # ── Arbitraj fırsatları ────────────────────────
159
- firsatlar = []
160
- for coin, borsa_fiyatlari in fiyatlar.items():
161
- gecerli = {b: p for b, p in borsa_fiyatlari.items() if p is not None}
162
- if len(gecerli) < 2:
163
- continue
164
- borsalar_list = list(gecerli.items())
165
- for i in range(len(borsalar_list)):
166
- for j in range(len(borsalar_list)):
167
- if i == j:
168
- continue
169
- al_borsa, al_fiyat = borsalar_list[i]
170
- sat_borsa, sat_fiyat = borsalar_list[j]
171
- if sat_fiyat <= al_fiyat:
172
- continue
173
- brut_fark = (sat_fiyat - al_fiyat) / al_fiyat * 100
174
- net_fark = brut_fark - (islem_ucreti * 2)
175
- if net_fark >= min_fark:
176
- firsatlar.append({
177
- "coin": coin,
178
- "al_borsa": al_borsa,
179
- "al_fiyat": al_fiyat,
180
- "sat_borsa": sat_borsa,
181
- "sat_fiyat": sat_fiyat,
182
- "brut_fark": brut_fark,
183
- "net_fark": net_fark,
184
- })
185
-
186
- firsatlar.sort(key=lambda x: x["net_fark"], reverse=True)
187
-
188
- if firsatlar:
189
- firsat_satirlari = ""
190
- for f in firsatlar:
191
- seviye = "🟢" if f["net_fark"] >= 1.0 else "🟡"
192
- firsat_satirlari += f"""
193
- <tr>
194
- <td><strong>{f['coin']}</strong></td>
195
- <td style="color:#4ade80">{f['al_borsa']}<br><small>${f['al_fiyat']:,.4f}</small></td>
196
- <td style="color:#f87171">{f['sat_borsa']}<br><small>${f['sat_fiyat']:,.4f}</small></td>
197
- <td>{f['brut_fark']:.3f}%</td>
198
- <td><strong>{seviye} {f['net_fark']:.3f}%</strong></td>
199
- </tr>"""
200
- firsat_html = f"""
201
- <div class="tablo-kap firsat-kap">
202
- <h3 class="tablo-baslik">⚡ Arbitraj Fırsatları ({len(firsatlar)} adet)</h3>
203
- <table class="arb-tablo">
204
- <thead><tr>
205
- <th>Coin</th><th>Al (Borsa)</th><th>Sat (Borsa)</th>
206
- <th>Brüt Fark</th><th>Net Fark</th>
207
- </tr></thead>
208
- <tbody>{firsat_satirlari}</tbody>
209
- </table>
210
- </div>"""
211
- durum = f"✅ {len(firsatlar)} fırsat bulundu | Son güncelleme: {zaman}"
212
  else:
213
- firsat_html = """
214
- <div class="tablo-kap bos-kap">
215
- <p style="text-align:center;padding:2rem;color:#94a3b8;">
216
- 😴 Eşik değerini karşılayan arbitraj fırsatı bulunamadı.<br>
217
- <small>Min. net farkı düşürmeyi veya daha fazla borsa seçmeyi deneyin.</small>
218
- </p>
219
- </div>"""
220
- durum = f"😴 Fırsat bulunamadı | Son güncelleme: {zaman}"
221
-
222
- return durum, fiyat_tablosu, firsat_html
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
 
225
- # ─────────────────────────────────────────────
226
- # CSS & TEMA
227
- # ─────────────────────────────────────────────
228
 
229
- CSS = """
230
- @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=Space+Grotesk:wght@400;600;700&display=swap');
231
-
232
- :root {
233
- --bg: #0a0e1a;
234
- --surface: #111827;
235
- --border: #1e2d40;
236
- --accent: #00d4ff;
237
- --accent2: #7c3aed;
238
- --green: #4ade80;
239
- --red: #f87171;
240
- --yellow: #fbbf24;
241
- --text: #e2e8f0;
242
- --muted: #64748b;
243
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
 
245
- body, .gradio-container {
246
- background: var(--bg) !important;
247
- font-family: 'Space Grotesk', sans-serif !important;
248
- color: var(--text) !important;
249
- }
250
 
251
- .baslik-blok {
252
- background: linear-gradient(135deg, #0f172a 0%, #1a1040 50%, #0f172a 100%);
253
- border: 1px solid var(--border);
254
- border-radius: 12px;
255
- padding: 1.5rem 2rem;
256
- margin-bottom: 1rem;
257
- position: relative;
258
- overflow: hidden;
259
- }
260
- .baslik-blok::before {
261
- content: '';
262
- position: absolute;
263
- top: 0; left: 0; right: 0;
264
- height: 2px;
265
- background: linear-gradient(90deg, var(--accent2), var(--accent), var(--green));
266
- }
267
- .baslik-blok h1 {
268
- font-size: 1.8rem;
269
- font-weight: 700;
270
- background: linear-gradient(90deg, var(--accent), var(--green));
271
- -webkit-background-clip: text;
272
- -webkit-text-fill-color: transparent;
273
- margin: 0 0 0.25rem 0;
274
- }
275
- .baslik-blok p {
276
- color: var(--muted);
277
- font-size: 0.85rem;
278
- margin: 0;
279
- font-family: 'IBM Plex Mono', monospace;
280
- }
281
 
282
- /* Tablo stili */
283
- .tablo-kap { margin-top: 1rem; }
284
- .tablo-baslik {
285
- font-size: 1rem;
286
- font-weight: 600;
287
- color: var(--accent);
288
- margin-bottom: 0.75rem;
289
- font-family: 'IBM Plex Mono', monospace;
290
- }
291
- .arb-tablo {
292
- width: 100%;
293
- border-collapse: collapse;
294
- font-size: 0.85rem;
295
- font-family: 'IBM Plex Mono', monospace;
296
- }
297
- .arb-tablo th {
298
- background: #1e2d40;
299
- color: var(--accent);
300
- padding: 0.6rem 1rem;
301
- text-align: left;
302
- font-weight: 600;
303
- border-bottom: 1px solid var(--border);
304
- }
305
- .arb-tablo td {
306
- padding: 0.55rem 1rem;
307
- border-bottom: 1px solid #1a2535;
308
- color: var(--text);
309
- }
310
- .arb-tablo tr:hover td { background: #141f2e; }
311
- .firsat-kap .tablo-baslik { color: var(--green); }
312
- .bos-kap { background: #0d1525; border-radius: 8px; border: 1px dashed var(--border); }
313
-
314
- /* Durum çubuğu */
315
- #durum-kutusu textarea {
316
- background: #0d1a2a !important;
317
- border: 1px solid var(--border) !important;
318
- color: var(--accent) !important;
319
- font-family: 'IBM Plex Mono', monospace !important;
320
- font-size: 0.82rem !important;
321
- }
322
 
323
- /* Buton */
324
- #tara-btn {
325
- background: linear-gradient(135deg, var(--accent2), var(--accent)) !important;
326
- border: none !important;
327
- font-weight: 700 !important;
328
- font-size: 1rem !important;
329
- padding: 0.8rem !important;
330
- border-radius: 8px !important;
331
- color: white !important;
332
- cursor: pointer !important;
333
- transition: opacity 0.2s !important;
334
- }
335
- #tara-btn:hover { opacity: 0.85 !important; }
336
 
337
- /* Panel */
338
- .panel {
339
- background: var(--surface) !important;
340
- border: 1px solid var(--border) !important;
341
- border-radius: 10px !important;
342
- }
343
 
344
- label { color: var(--muted) !important; font-size: 0.82rem !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
 
346
- .gr-checkbox-group label, .gr-radio label { color: var(--text) !important; }
347
 
348
- input[type=number], .gr-slider { accent-color: var(--accent) !important; }
349
- """
 
350
 
351
- # ─────────────────────────────────────────────
352
- # GRADIO ARAYÜZÜ
353
- # ─────────────────────────────────────────────
354
 
355
- def uygulama_olustur():
356
- with gr.Blocks(css=CSS, title="Kripto Arbitraj Tarayıcı") as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
 
358
- gr.HTML("""
359
- <div class="baslik-blok">
360
- <h1>⚡ Kripto Arbitraj Tarayıcı</h1>
361
- <p>Borsalar arası fiyat farklarını gerçek zamanlı tespit et • USDT çiftleri • Türkçe arayüz</p>
362
- </div>
363
- """)
364
-
365
- with gr.Row():
366
- # ── Sol panel: Ayarlar ──────────────────
367
- with gr.Column(scale=1, elem_classes="panel"):
368
- gr.Markdown("### 🎛��� Ayarlar")
369
-
370
- secili_coinler = gr.CheckboxGroup(
371
- choices=DESTEKLENEN_COINLER,
372
- value=["BTC", "ETH", "SOL", "BNB"],
373
- label="Coin Seçimi",
374
- info="Taranacak kripto para birimleri",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
375
  )
376
-
377
- secili_borsalar = gr.CheckboxGroup(
378
- choices=list(BORSA_FONKSIYONLARI.keys()),
379
- value=["Binance", "KuCoin", "Bybit"],
380
- label="Borsa Seçimi",
381
- info="En az 2 borsa seçin",
382
  )
383
-
384
- min_fark = gr.Slider(
385
- minimum=0.0,
386
- maximum=5.0,
387
- value=0.3,
388
- step=0.05,
389
- label="Min. Net Fark (%)",
390
- info="Bu değerin altındaki fırsatlar gizlenir",
391
  )
392
 
393
- islem_ucreti = gr.Slider(
394
- minimum=0.0,
395
- maximum=1.0,
396
- value=0.1,
397
- step=0.01,
398
- label="İşlem Ücreti (% / borsa)",
399
- info="Her iki taraf için toplam 2× uygulanır",
400
- )
401
 
402
- tara_btn = gr.Button(
403
- "🚀 Tara",
404
- variant="primary",
405
- elem_id="tara-btn",
406
  )
407
-
408
- durum = gr.Textbox(
409
- label="Durum",
410
- interactive=False,
411
- elem_id="durum-kutusu",
412
- lines=2,
 
 
 
413
  )
414
 
415
- gr.Markdown("""
416
- ---
417
- **⚠️ Uyarı:** Bu araç yalnızca bilgi amaçlıdır.
418
- Arbitraj işlemleri transfer süresi, likidite
419
- ve ağ ücretlerinden etkilenebilir.
420
- """)
421
-
422
- # ── Sağ panel: Sonuçlar ─────────────────
423
- with gr.Column(scale=3):
424
- fiyat_html = gr.HTML(label="Fiyatlar")
425
- firsat_html = gr.HTML(label="Fırsatlar")
426
-
427
- # ── Bağlantılar ─────────────────────────────
428
- tara_btn.click(
429
- fn=arbitraj_hesapla,
430
- inputs=[secili_coinler, secili_borsalar, min_fark, islem_ucreti],
431
- outputs=[durum, fiyat_html, firsat_html],
432
- )
433
 
434
- return demo
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
435
 
436
 
437
  if __name__ == "__main__":
438
- app = uygulama_olustur()
439
- app.launch(
440
- server_name="0.0.0.0",
441
- server_port=7860,
442
- show_api=False,
443
- share=False,
444
- )
 
 
 
 
 
 
1
  import gradio as gr
2
  import requests
3
  import pandas as pd
4
+ import numpy as np
5
  from datetime import datetime
6
+ import time
7
 
8
+ # ──────────────────────────────────────────────────────────
9
+ # COIN LİSTESİ (CoinGecko ID → Sembol)
10
+ # ──────────────────────────────────────────────────────────
11
+ COINS = {
12
+ "bitcoin": "BTC",
13
+ "ethereum": "ETH",
14
+ "binancecoin": "BNB",
15
+ "ripple": "XRP",
16
+ "solana": "SOL",
17
+ "cardano": "ADA",
18
+ "dogecoin": "DOGE",
19
+ "avalanche-2": "AVAX",
20
+ "chainlink": "LINK",
21
+ "polkadot": "DOT",
22
+ "matic-network": "MATIC",
23
+ "tron": "TRX",
24
+ "litecoin": "LTC",
25
+ "uniswap": "UNI",
26
+ "cosmos": "ATOM",
27
+ "near": "NEAR",
28
+ "algorand": "ALGO",
29
+ "stellar": "XLM",
30
+ "filecoin": "FIL",
31
+ "internet-computer": "ICP",
32
+ "aptos": "APT",
33
+ "arbitrum": "ARB",
34
+ "optimism": "OP",
35
+ "sui": "SUI",
36
+ "pepe": "PEPE",
37
+ "shiba-inu": "SHIB",
38
+ "the-sandbox": "SAND",
39
+ "decentraland": "MANA",
40
+ "aave": "AAVE",
41
+ "maker": "MKR",
42
  }
43
 
44
+ BASE_URL = "https://api.coingecko.com/api/v3"
45
+ HEADERS = {"accept": "application/json"}
 
 
 
46
 
47
+ # ──────────────────────────────────────────────────────────
48
+ # VERİ ÇEKME
49
+ # ──────────────────────────────────────────────────────────
50
 
51
+ def fetch_ohlc(coin_id: str, days: int = 30) -> pd.Series | None:
52
+ """CoinGecko OHLC endpoint – günlük kapanış fiyatları."""
53
+ url = f"{BASE_URL}/coins/{coin_id}/ohlc?vs_currency=usd&days={days}"
54
  try:
55
+ r = requests.get(url, headers=HEADERS, timeout=20)
56
+ r.raise_for_status()
57
+ data = r.json()
58
+ if not data:
59
+ return None
60
+ df = pd.DataFrame(data, columns=["ts", "open", "high", "low", "close"])
61
+ df["ts"] = pd.to_datetime(df["ts"], unit="ms")
62
+ df = df.set_index("ts").resample("D").last().dropna()
63
+ return df["close"]
64
  except Exception:
65
  return None
66
 
67
 
68
+ def fetch_market_chart(coin_id: str, days: int = 30) -> pd.Series | None:
69
+ """Fallback: /market_chart ile günlük fiyat."""
70
+ url = f"{BASE_URL}/coins/{coin_id}/market_chart?vs_currency=usd&days={days}&interval=daily"
71
  try:
72
+ r = requests.get(url, headers=HEADERS, timeout=20)
73
+ r.raise_for_status()
74
+ data = r.json().get("prices", [])
75
+ if not data:
76
+ return None
77
+ df = pd.DataFrame(data, columns=["ts", "price"])
78
+ df["ts"] = pd.to_datetime(df["ts"], unit="ms")
79
+ df = df.set_index("ts").resample("D").last().dropna()
80
+ return df["price"]
81
  except Exception:
82
  return None
83
 
84
 
85
+ def get_price_series(coin_id: str, days: int) -> pd.Series | None:
86
+ s = fetch_ohlc(coin_id, days)
87
+ if s is None or len(s) < 5:
88
+ s = fetch_market_chart(coin_id, days)
89
+ return s if s is not None and len(s) >= 5 else None
 
 
 
 
 
 
 
90
 
91
 
92
+ # ──────────────────────────────────────────────────────────
93
+ # HESAPLAMALAR
94
+ # ──────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
95
 
96
+ def returns(series: pd.Series) -> pd.Series:
97
+ return series.pct_change().dropna()
98
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
+ def rolling_corr(s1: pd.Series, s2: pd.Series, window: int = 14) -> float:
101
+ """Son N günlük rolling correlation."""
102
+ r1 = returns(s1)
103
+ r2 = returns(s2)
104
+ combined = pd.concat([r1, r2], axis=1).dropna()
105
+ if len(combined) < window:
106
+ return float(combined.iloc[:, 0].corr(combined.iloc[:, 1]))
107
+ tail = combined.tail(window)
108
+ return float(tail.iloc[:, 0].corr(tail.iloc[:, 1]))
109
 
 
 
 
 
 
 
 
110
 
111
+ def zscore_spread(s1: pd.Series, s2: pd.Series, window: int = 20) -> dict | None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  """
113
+ Pairs trading Z-score.
114
+ ratio = log(s1) - log(s2)
115
+ z = (ratio - mean) / std (rolling window)
116
  """
117
+ log1 = np.log(s1)
118
+ log2 = np.log(s2)
119
+ ratio = (log1 - log2).dropna()
120
+ if len(ratio) < window:
121
+ window = max(5, len(ratio) // 2)
122
+ roll_mean = ratio.rolling(window).mean()
123
+ roll_std = ratio.rolling(window).std()
124
+ zscore = ((ratio - roll_mean) / roll_std).dropna()
125
+ if len(zscore) == 0:
126
+ return None
127
+ current_z = float(zscore.iloc[-1])
128
+ current_ratio = float(ratio.iloc[-1])
129
+ ratio_mean = float(ratio.mean())
130
+ ratio_std = float(ratio.std())
131
+ half_life = _half_life(ratio)
132
+ return {
133
+ "current_z": current_z,
134
+ "current_ratio": current_ratio,
135
+ "ratio_mean": ratio_mean,
136
+ "ratio_std": ratio_std,
137
+ "half_life": half_life,
138
+ }
139
+
140
+
141
+ def _half_life(spread: pd.Series) -> float:
142
+ """Ornstein-Uhlenbeck half-life (OLS)."""
143
+ try:
144
+ lag = spread.shift(1).dropna()
145
+ delta = spread.diff().dropna()
146
+ combined = pd.concat([lag, delta], axis=1).dropna()
147
+ if len(combined) < 5:
148
+ return float("nan")
149
+ x = combined.iloc[:, 0].values
150
+ y = combined.iloc[:, 1].values
151
+ A = np.vstack([np.ones(len(x)), x]).T
152
+ result = np.linalg.lstsq(A, y, rcond=None)
153
+ b = result[0][1]
154
+ if b >= 0:
155
+ return float("nan")
156
+ return round(-np.log(2) / b, 1)
157
+ except Exception:
158
+ return float("nan")
159
+
160
+
161
+ def trade_signal(z: float, corr: float, half_life: float) -> tuple[str, str]:
162
+ """Sinyal üret."""
163
+ if abs(z) < 0.5:
164
+ return "NÖTR", "#555577"
165
+ if corr < 0.5:
166
+ return "DÜŞÜK KOR.", "#443344"
167
+ hl_ok = not np.isnan(half_life) and 2 <= half_life <= 60
168
+ if abs(z) >= 2.5 and hl_ok:
169
+ strength = "GÜÇLÜ"
170
+ elif abs(z) >= 1.5:
171
+ strength = "ORTA"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  else:
173
+ strength = "İZLE"
174
+ if z > 2.0:
175
+ return f"{strength} · 1.SAT / 2.AL", "red"
176
+ elif z < -2.0:
177
+ return f"{strength} · 1.AL / 2.SAT", "green"
178
+ elif z > 1.0:
179
+ return f"{strength} · 1.SAT / 2.AL", "orange"
180
+ else:
181
+ return f"{strength} · 1.AL / 2.SAT", "yellow"
182
+
183
+
184
+ # ──────────────────────────────────────────────────────────
185
+ # ANA TARAMA
186
+ # ──────────────────────────────────────────────────────────
187
+
188
+ def scan_correlations(
189
+ selected_coins: list,
190
+ days: int,
191
+ min_corr: float,
192
+ min_abs_z: float,
193
+ progress=gr.Progress(),
194
+ ):
195
+ if not selected_coins or len(selected_coins) < 2:
196
+ return _err_html("En az 2 coin seçin!"), None, "Tarama yapılmadı."
197
+
198
+ coin_ids = [k for k, v in COINS.items() if v in selected_coins]
199
+ n = len(coin_ids)
200
+
201
+ # 1) Fiyat serilerini çek
202
+ price_data = {}
203
+ for i, cid in enumerate(coin_ids):
204
+ sym = COINS[cid]
205
+ progress((i + 1) / (n + 1), desc=f"{sym} fiyatları çekiliyor...")
206
+ s = get_price_series(cid, days)
207
+ if s is not None:
208
+ price_data[sym] = s
209
+ time.sleep(0.35)
210
+
211
+ available = list(price_data.keys())
212
+ if len(available) < 2:
213
+ return _err_html("Yeterli fiyat verisi alınamadı. Tekrar deneyin."), None, "Hata."
214
+
215
+ # 2) Tüm çiftleri analiz et
216
+ pairs = []
217
+ total_pairs = len(available) * (len(available) - 1) // 2
218
+ idx = 0
219
+ for i in range(len(available)):
220
+ for j in range(i + 1, len(available)):
221
+ idx += 1
222
+ progress(idx / max(total_pairs, 1), desc=f"{available[i]}-{available[j]} analiz...")
223
+ s1 = price_data[available[i]]
224
+ s2 = price_data[available[j]]
225
+
226
+ common = s1.index.intersection(s2.index)
227
+ if len(common) < 10:
228
+ continue
229
+ s1c = s1.loc[common]
230
+ s2c = s2.loc[common]
231
+
232
+ corr = rolling_corr(s1c, s2c, window=min(14, len(common) - 1))
233
+ if abs(corr) < min_corr:
234
+ continue
235
+
236
+ spread_info = zscore_spread(s1c, s2c, window=min(20, len(common) - 2))
237
+ if spread_info is None:
238
+ continue
239
+
240
+ z = spread_info["current_z"]
241
+ if abs(z) < min_abs_z:
242
+ continue
243
+
244
+ hl = spread_info["half_life"]
245
+ signal, _ = trade_signal(z, corr, hl)
246
+
247
+ pairs.append({
248
+ "Coin 1": available[i],
249
+ "Coin 2": available[j],
250
+ "Korelasyon": round(corr, 4),
251
+ "Z-Score": round(z, 3),
252
+ "Half-Life (gün)": round(hl, 1) if not np.isnan(hl) else "-",
253
+ "Ratio (log)": round(spread_info["current_ratio"], 5),
254
+ "Ratio Ort.": round(spread_info["ratio_mean"], 5),
255
+ "Sinyal": signal,
256
+ })
257
+
258
+ if not pairs:
259
+ return _empty_html(min_corr, min_abs_z), None, f"Uygun çift bulunamadı (corr≥{min_corr}, |z|≥{min_abs_z})."
260
+
261
+ df = (
262
+ pd.DataFrame(pairs)
263
+ .sort_values("Z-Score", key=abs, ascending=False)
264
+ .reset_index(drop=True)
265
+ )
266
 
267
+ html = _render_html(df)
268
+ log = _make_log(df, days, available)
269
+ return html, df, log
270
+
271
+
272
+ # ──────────────────────────────────────────────────────────
273
+ # HTML RENDER
274
+ # ──────────────────────────────────────────────────────────
275
+
276
+ def _signal_style(sig: str) -> str:
277
+ if "GÜÇLÜ" in sig and "SAT" in sig:
278
+ return "background:#ff6b6b22;color:#ff6b6b;border:1px solid #ff6b6b55"
279
+ if "GÜÇLÜ" in sig and "AL" in sig:
280
+ return "background:#00ff8822;color:#00ff88;border:1px solid #00ff8855"
281
+ if "ORTA" in sig and "SAT" in sig:
282
+ return "background:#ff9f4322;color:#ff9f43;border:1px solid #ff9f4355"
283
+ if "ORTA" in sig and "AL" in sig:
284
+ return "background:#ffd70022;color:#ffd700;border:1px solid #ffd70055"
285
+ if "İZLE" in sig:
286
+ return "background:#7b7bff22;color:#7b7bff;border:1px solid #7b7bff55"
287
+ return "color:#555;border:1px solid #333"
288
+
289
+
290
+ def _corr_bar(c: float) -> str:
291
+ pct = int(abs(c) * 100)
292
+ color = "#00d2ff" if c > 0 else "#ff6b9d"
293
+ return (
294
+ f"<div style='display:flex;align-items:center;gap:8px'>"
295
+ f"<div style='width:64px;height:5px;background:#1a1a3a;border-radius:3px'>"
296
+ f"<div style='width:{pct}%;height:100%;background:{color};border-radius:3px'></div></div>"
297
+ f"<span style='color:{color};font-size:13px;font-weight:700'>{c:+.3f}</span></div>"
298
+ )
299
 
 
 
 
300
 
301
+ def _z_badge(z: float) -> str:
302
+ az = abs(z)
303
+ if az >= 2.5:
304
+ c, sz = ("#ff4444" if z > 0 else "#00ff88"), "17px"
305
+ elif az >= 1.5:
306
+ c, sz = ("#ff9f43" if z > 0 else "#ffd700"), "15px"
307
+ else:
308
+ c, sz = "#7b7bff", "13px"
309
+ arrow = "▲" if z > 0 else "▼"
310
+ return f"<span style='color:{c};font-size:{sz};font-weight:800'>{arrow} {z:+.3f}</span>"
311
+
312
+
313
+ def _hl_badge(hl) -> str:
314
+ if hl == "-" or (isinstance(hl, float) and np.isnan(hl)):
315
+ return "<span style='color:#444'>—</span>"
316
+ hl = float(hl)
317
+ if 2 <= hl <= 60:
318
+ c = "#00ff88"
319
+ elif hl < 2:
320
+ c = "#ff6b6b"
321
+ else:
322
+ c = "#555"
323
+ return f"<span style='color:{c};font-family:monospace'>{hl:.1f} gün</span>"
324
+
325
+
326
+ def _render_html(df: pd.DataFrame) -> str:
327
+ rows = ""
328
+ for rank, (_, row) in enumerate(df.iterrows(), 1):
329
+ sig = row["Sinyal"]
330
+ sstyle = _signal_style(sig)
331
+ rows += f"""
332
+ <tr>
333
+ <td style='color:#555;font-size:11px;text-align:center'>{rank}</td>
334
+ <td>
335
+ <span style='color:#c0c0ff;font-weight:700;font-size:14px'>{row['Coin 1']}</span>
336
+ <span style='color:#2a2a5a;margin:0 8px;font-size:18px'>⟷</span>
337
+ <span style='color:#c0c0ff;font-weight:700;font-size:14px'>{row['Coin 2']}</span>
338
+ </td>
339
+ <td>{_corr_bar(row['Korelasyon'])}</td>
340
+ <td>{_z_badge(row['Z-Score'])}</td>
341
+ <td>{_hl_badge(row['Half-Life (gün)'])}</td>
342
+ <td>
343
+ <span style='display:inline-block;padding:5px 12px;border-radius:6px;
344
+ font-size:11px;font-weight:700;letter-spacing:.5px;white-space:nowrap;{sstyle}'>
345
+ {sig}
346
+ </span>
347
+ </td>
348
+ </tr>"""
349
+
350
+ strong = len(df[df["Sinyal"].str.contains("GÜÇLÜ", na=False)])
351
+ orta = len(df[df["Sinyal"].str.contains("ORTA", na=False)])
352
+ izle = len(df[df["Sinyal"].str.contains("İZLE", na=False)])
353
+
354
+ return f"""
355
+ <style>
356
+ @import url('https://fonts.googleapis.com/css2?family=Space+Mono&family=Syne:wght@400;700;800&display=swap');
357
+ .ct-wrap {{ background:#06060f;border-radius:14px;overflow:hidden;font-family:'Syne',sans-serif }}
358
+ .ct-hdr {{ background:linear-gradient(135deg,#0d0d2a,#12122a);padding:16px 22px;
359
+ display:flex;justify-content:space-between;align-items:center;
360
+ border-bottom:1px solid #1a1a3a }}
361
+ .ct-title {{ font-size:11px;font-weight:800;letter-spacing:3px;color:#7b7bff }}
362
+ .ct-stats {{ display:flex;gap:20px }}
363
+ .ct-sv {{ font-size:22px;font-weight:800;text-align:center }}
364
+ .ct-sl {{ font-size:9px;color:#444;letter-spacing:1.5px;text-align:center }}
365
+ .ct-tbl {{ width:100%;border-collapse:collapse }}
366
+ .ct-tbl th {{ background:#0d0d22;color:#5555aa;padding:9px 14px;
367
+ font-size:9px;letter-spacing:2px;text-transform:uppercase;
368
+ text-align:left;border-bottom:1px solid #1a1a3a }}
369
+ .ct-tbl td {{ padding:12px 14px;border-bottom:1px solid #0e0e22;vertical-align:middle }}
370
+ .ct-tbl tr:hover td {{ background:#090918 }}
371
+ .ct-tbl tr:last-child td {{ border-bottom:none }}
372
+ </style>
373
+ <div class='ct-wrap'>
374
+ <div class='ct-hdr'>
375
+ <span class='ct-title'>⚡ KORELASYon TİCARETİ &nbsp;·&nbsp; {datetime.now().strftime('%H:%M:%S')}</span>
376
+ <div class='ct-stats'>
377
+ <div><div class='ct-sv' style='color:#ff6b6b'>{strong}</div><div class='ct-sl'>GÜÇLÜ</div></div>
378
+ <div><div class='ct-sv' style='color:#ffd700'>{orta}</div><div class='ct-sl'>ORTA</div></div>
379
+ <div><div class='ct-sv' style='color:#7b7bff'>{izle}</div><div class='ct-sl'>İZLE</div></div>
380
+ <div><div class='ct-sv' style='color:#aaa'>{len(df)}</div><div class='ct-sl'>TOPLAM</div></div>
381
+ </div>
382
+ </div>
383
+ <table class='ct-tbl'>
384
+ <thead><tr>
385
+ <th>#</th><th>Çift</th><th>Korelasyon</th>
386
+ <th>Z-Score</th><th>Half-Life</th><th>Sinyal</th>
387
+ </tr></thead>
388
+ <tbody>{rows}</tbody>
389
+ </table>
390
+ </div>"""
391
 
 
 
 
 
 
392
 
393
+ def _err_html(msg: str) -> str:
394
+ return (
395
+ f"<div style='background:#1a0a0a;border:1px solid #ff4444;border-radius:10px;"
396
+ f"padding:30px;text-align:center;color:#ff6b6b;font-family:monospace'>⚠️ {msg}</div>"
397
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
398
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
 
400
+ def _empty_html(min_corr: float, min_abs_z: float) -> str:
401
+ return (
402
+ f"<div style='background:#06060f;border:1px solid #1a1a3a;border-radius:12px;"
403
+ f"padding:50px;text-align:center;font-family:monospace'>"
404
+ f"<div style='font-size:40px'>📭</div>"
405
+ f"<div style='color:#7b7bff;margin-top:14px'>corr≥{min_corr} ve |Z|≥{min_abs_z} koşullarını sağlayan çift bulunamadı.</div>"
406
+ f"<div style='color:#444;margin-top:8px;font-size:12px'>Eşikleri düşürün veya daha fazla coin seçin.</div></div>"
407
+ )
 
 
 
 
 
408
 
 
 
 
 
 
 
409
 
410
+ def _make_log(df: pd.DataFrame, days: int, available: list) -> str:
411
+ lines = [
412
+ f"[{datetime.now().strftime('%H:%M:%S')}] Tarama tamamlandı",
413
+ f" Analiz edilen coinler : {', '.join(available)}",
414
+ f" Tarihsel veri süresi : {days} gün",
415
+ f" Bulunan çift sayısı : {len(df)}",
416
+ "─" * 60,
417
+ ]
418
+ for _, row in df.iterrows():
419
+ lines.append(
420
+ f" {row['Coin 1']:<6} ↔ {row['Coin 2']:<6} "
421
+ f"corr={row['Korelasyon']:+.3f} z={row['Z-Score']:+.3f} "
422
+ f"HL={row['Half-Life (gün)']} gün → {row['Sinyal']}"
423
+ )
424
+ return "\n".join(lines)
425
 
 
426
 
427
+ # ──────────────────────────────────────────────────────────
428
+ # GRADIO ARAYÜZÜ
429
+ # ──────────────────────────────────────────────────────────
430
 
431
+ ALL_SYMBOLS = sorted(COINS.values())
432
+ DEFAULT_COINS = ["BTC", "ETH", "BNB", "SOL", "AVAX", "LINK", "ADA", "MATIC", "DOT", "ATOM"]
 
433
 
434
+ CSS = """
435
+ @import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;700;800&display=swap');
436
+ body, .gradio-container {
437
+ background:#06060f !important;
438
+ color:#e0e0ff !important;
439
+ font-family:'Syne',sans-serif !important;
440
+ }
441
+ .gr-button { font-family:'Syne',sans-serif !important; font-weight:700 !important; }
442
+ .gr-button-primary {
443
+ background:linear-gradient(135deg,#7b7bff,#00d2ff) !important;
444
+ color:#000 !important; border:none !important; letter-spacing:1px !important;
445
+ }
446
+ .gr-button-secondary {
447
+ background:#0d0d22 !important;
448
+ color:#7b7bff !important;
449
+ border:1px solid #2a2a4a !important;
450
+ }
451
+ label { color:#aaa !important; font-size:11px !important; letter-spacing:1px !important; }
452
+ .gr-panel, .gr-box { background:#0a0a1a !important; border:1px solid #1a1a3a !important; }
453
+ footer { display:none !important; }
454
+ """
455
 
456
+ HEADER_HTML = """
457
+ <div style='text-align:center;padding:28px 0 12px;font-family:Syne,sans-serif'>
458
+ <div style='
459
+ font-size:26px;font-weight:800;letter-spacing:5px;
460
+ background:linear-gradient(90deg,#7b7bff,#00d2ff,#00ff88);
461
+ -webkit-background-clip:text;-webkit-text-fill-color:transparent
462
+ '>KORELASYON TRADİNG SKANER</div>
463
+ <div style='color:#333;font-size:11px;letter-spacing:3px;margin-top:5px'>
464
+ PAIRS TRADING · Z-SCORE · MEAN REVERSION · COINGECKO · API ANAHTARI YOK
465
+ </div>
466
+ </div>"""
467
+
468
+ INFO_HTML = """
469
+ <div style='background:#0a0a1e;border:1px solid #1a1a3a;border-radius:10px;
470
+ padding:14px 16px;font-size:12px;color:#666;line-height:2;margin-top:4px'>
471
+ <div style='color:#7b7bff;font-weight:700;letter-spacing:1px;margin-bottom:4px'>📐 ALGORİTMA</div>
472
+ <b style='color:#aaa'>Z-Score</b> = (log_ratio − ort) / std<br>
473
+ <b style='color:#aaa'>Half-Life</b> = OU modeli ile mean-reversion hızı<br>
474
+ <span style='color:#00ff88'>|Z| &gt; 2</span> → İşlem sinyali<br>
475
+ <span style='color:#ffd700'>2 – 60 gün</span> → İdeal half-life aralığı<br>
476
+ <span style='color:#ff6b6b'>Korelasyon &gt; 0.7</span> → Güvenilir çift
477
+ </div>"""
478
+
479
+
480
+ def export_csv(df):
481
+ if df is None:
482
+ return None
483
+ path = "/tmp/korelasyon_sonuclari.csv"
484
+ df.to_csv(path, index=False, encoding="utf-8-sig")
485
+ return path
486
+
487
+
488
+ with gr.Blocks(css=CSS, title="Korelasyon Trading Skaner") as demo:
489
+ gr.HTML(HEADER_HTML)
490
+
491
+ with gr.Row():
492
+ # ── Sol panel: coin seçimi ──
493
+ with gr.Column(scale=1, min_width=200):
494
+ coin_selector = gr.CheckboxGroup(
495
+ choices=ALL_SYMBOLS,
496
+ value=DEFAULT_COINS,
497
+ label="COİN SEÇİMİ",
498
+ )
499
+ gr.HTML(INFO_HTML)
500
+
501
+ # ── Sağ panel: kontroller + sonuç ──
502
+ with gr.Column(scale=3):
503
+ with gr.Row():
504
+ days_slider = gr.Slider(
505
+ minimum=14, maximum=90, value=30, step=7,
506
+ label="VERİ SÜRESİ (GÜN)",
507
  )
508
+ corr_slider = gr.Slider(
509
+ minimum=0.3, maximum=0.99, value=0.65, step=0.05,
510
+ label="MİN. KORELASYON",
 
 
 
511
  )
512
+ z_slider = gr.Slider(
513
+ minimum=0.5, maximum=4.0, value=1.5, step=0.25,
514
+ label="MİN. |Z-SCORE|",
 
 
 
 
 
515
  )
516
 
517
+ with gr.Row():
518
+ scan_btn = gr.Button("⚡ TARAMAYI BAŞLAT", variant="primary", size="lg")
519
+ clear_btn = gr.Button("🗑 TEMİZLE", variant="secondary")
520
+ export_btn = gr.Button("📥 CSV İNDİR", variant="secondary")
 
 
 
 
521
 
522
+ result_html = gr.HTML(
523
+ value=(
524
+ "<div style='color:#222;text-align:center;padding:70px;"
525
+ "font-family:monospace;font-size:13px'>Coin seçip taramayı başlatın →</div>"
526
  )
527
+ )
528
+
529
+ with gr.Accordion("📋 LOG / DETAY", open=False):
530
+ log_box = gr.Textbox(
531
+ label="",
532
+ lines=14,
533
+ max_lines=22,
534
+ placeholder="Tarama logları burada görünür...",
535
+ show_copy_button=True,
536
  )
537
 
538
+ df_state = gr.State(None)
539
+ csv_file = gr.File(label="CSV Dosyası", visible=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
540
 
541
+ # ── Bağlantılar ──
542
+ scan_btn.click(
543
+ fn=scan_correlations,
544
+ inputs=[coin_selector, days_slider, corr_slider, z_slider],
545
+ outputs=[result_html, df_state, log_box],
546
+ )
547
+
548
+ clear_btn.click(
549
+ fn=lambda: (
550
+ "<div style='color:#222;text-align:center;padding:70px;font-family:monospace'>Temizlendi.</div>",
551
+ None,
552
+ "",
553
+ ),
554
+ outputs=[result_html, df_state, log_box],
555
+ )
556
+
557
+ export_btn.click(
558
+ fn=export_csv,
559
+ inputs=[df_state],
560
+ outputs=[csv_file],
561
+ ).then(fn=lambda: gr.update(visible=True), outputs=[csv_file])
562
 
563
 
564
  if __name__ == "__main__":
565
+ demo.launch()