GoshawkVortexAI commited on
Commit
c7cb101
·
verified ·
1 Parent(s): d67afaf

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +1865 -0
  2. requirements.txt +6 -0
app.py ADDED
@@ -0,0 +1,1865 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ OKX QUANT TERMINAL · v2 — ELITE SIGNAL ENGINE
3
+ Hedge-fund-style crypto market analytics dashboard built on Gradio.
4
+
5
+ Uses ONLY OKX's public (unauthenticated) market-data REST API:
6
+ - /api/v5/market/candles
7
+ - /api/v5/market/ticker
8
+ - /api/v5/market/tickers
9
+ - /api/v5/market/books
10
+ - /api/v5/public/funding-rate (türev duyarlılığı, isteğe bağlı)
11
+
12
+ No API key, no order placement, no account access. This is a read-only
13
+ analytics / signal-generation terminal, not an execution bot.
14
+
15
+ v2 SİNYAL MOTORU — YENİLİKLER
16
+ * Yeni faktörler: Supertrend, Ichimoku bulutu, Donchian kırılımı (hacim teyitli),
17
+ piyasa yapısı (HH/HL – LH/LL), CMF, MFI, mum formasyonları, Bollinger/Keltner sıkışması
18
+ * Rejim ağırlıkları artık ADX ile SÜREKLİ (yumuşak) geçiş yapar (eski: sert eşik)
19
+ * Skor tanh ile ±10'a yumuşak sıkıştırılır; "GÜÇLÜ" sinyal için faktör konsensüsü + MTF şartı
20
+ * 2 kademeli çoklu zaman dilimi (MTF) teyidi
21
+ * Canlı mikro-yapı katmanı: emir defteri dengesizliği, funding rate, yakın destek/direnç
22
+ * Yapısal (S/R) stop + ATR stop, 3 kademeli hedef, hesap büyüklüğüne göre pozisyon boyutu
23
+ * Gerçek işlem simülasyonu (SL/TP/zaman çıkışı + komisyon): kazanma oranı, profit factor,
24
+ beklenti, R çarpanı, SQN, drawdown, Al&Tut karşılaştırması, yarım-Kelly risk önerisi
25
+ * Bileşenli GÜVEN skoru + A+ … D kalite notu, faktör katkı grafiği, sinyal işaretli fiyat grafiği
26
+ * "Sadece kapanmış mumlar" modu (repaint yok), veri önbelleği, paralel veri çekimi
27
+
28
+ v3 SERT FİLTRELER — YÖN İSABETİNİ ARTIRMAYA YÖNELİK (sinyal sayısını azaltıp kaliteyi artırır)
29
+ * Rejim kapısı: ADX çok düşükken (belirgin trend yok) sinyal üretilmez
30
+ * Konsensüs kapısı: T/R faktörleri yeterince aynı yönde değilse (agree < eşik) sinyal BEKLE'ye düşer
31
+ * MTF sert kapısı: üst zaman dilimi net ters yöndeyse normal AL/SAT bile filtrelenir (GÜÇLÜ hariç)
32
+ * Emir defteri kapısı: defter sinyale güçlü ters yöndeyse sinyal filtrelenir
33
+ * Rejim + konsensüs kapıları canlı sinyal İLE backtest'te AYNI fonksiyonla uygulanır (tutarlılık)
34
+ * Backtest artık ilk-yarı/ikinci-yarı isabet oranını karşılaştırıp tutarsızlığı (rejim kayması /
35
+ olası aşırı-uyum) işaretliyor ve güven skorunu buna göre düşürüyor
36
+ * NOT: Bu filtreler sinyal kalitesini yükseltir ama hiçbir kombinasyon sürekli %80 isabeti garanti
37
+ edemez; kripto gibi gürültülü piyasalarda %55-60 tutarlı isabet + iyi R:R zaten güçlü kabul edilir
38
+ """
39
+
40
+ import math
41
+ import threading
42
+ import time
43
+ from concurrent.futures import ThreadPoolExecutor, as_completed
44
+
45
+ import numpy as np
46
+ import pandas as pd
47
+ import plotly.graph_objects as go
48
+ from plotly.subplots import make_subplots
49
+ import requests
50
+ import gradio as gr
51
+
52
+ try: # Hugging Face Spaces (ZeroGPU) — yerelde yoksa sessizce atla
53
+ import spaces
54
+ _gpu = spaces.GPU(duration=30)
55
+ except Exception:
56
+ def _gpu(fn):
57
+ return fn
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # CONFIG
61
+ # ---------------------------------------------------------------------------
62
+ OKX_BASE = "https://www.okx.com"
63
+ TIMEOUT = 10
64
+ CACHE_TTL = 8 # saniye — aynı mum verisini sekmeler arasında tekrar çekmemek için
65
+
66
+ POPULAR_SYMBOLS = [
67
+ "BTC-USDT", "ETH-USDT", "SOL-USDT", "BNB-USDT", "XRP-USDT",
68
+ "DOGE-USDT", "ADA-USDT", "AVAX-USDT", "LINK-USDT", "TON-USDT",
69
+ "TRX-USDT", "DOT-USDT", "MATIC-USDT", "LTC-USDT", "NEAR-USDT",
70
+ "SUI-USDT", "APT-USDT", "ARB-USDT", "OP-USDT", "INJ-USDT",
71
+ ]
72
+
73
+ BAR_OPTIONS = ["1m", "3m", "5m", "15m", "30m", "1H", "2H", "4H", "6H", "12H", "1D", "1W"]
74
+
75
+ # approximate periods-per-year for each bar size, used to annualize risk stats
76
+ BAR_TO_PERIODS_PER_YEAR = {
77
+ "1m": 365 * 24 * 60, "3m": 365 * 24 * 20, "5m": 365 * 24 * 12,
78
+ "15m": 365 * 24 * 4, "30m": 365 * 24 * 2, "1H": 365 * 24,
79
+ "2H": 365 * 12, "4H": 365 * 6, "6H": 365 * 4, "12H": 365 * 2,
80
+ "1D": 365, "1W": 52,
81
+ }
82
+
83
+ CHART_BG = "#0a0e14"
84
+
85
+ # her zaman dilimi için "bir üst" zaman dilimi -> çoklu zaman dilimi (MTF) teyidi için
86
+ MTF_MAP = {
87
+ "1m": "15m", "3m": "30m", "5m": "1H", "15m": "4H", "30m": "4H",
88
+ "1H": "4H", "2H": "1D", "4H": "1D", "6H": "1D", "12H": "1W",
89
+ "1D": "1W", "1W": "1W",
90
+ }
91
+ SLOW_BARS = {"4H", "6H", "12H", "1D", "1W"} # bu vadelerde emir defteri etkisi yarıya iner
92
+
93
+ # --- Sinyal motoru parametreleri ---
94
+ SIGNAL_THRESHOLD = 2.0 # |skor| >= bu değer -> AL / SAT
95
+ STRONG_THRESHOLD = 4.5 # |skor| >= bu değer + konsensüs -> GÜÇLÜ
96
+ WARMUP_BARS = 80 # Ichimoku (52+26) ısınması için minimum bar
97
+ BACKTEST_LOOKBACK = 220 # walk-forward test penceresi (bar)
98
+ SL_ATR, TP_ATR = 1.5, 2.5 # backtest ve varsayılan işlem planı (ATR çarpanları)
99
+ MAX_HOLD_BARS = 24 # zaman çıkışı
100
+ FEE_PER_SIDE = 0.001 # OKX spot taker ≈ %0.1 / yön
101
+ TP_R_MULTIPLES = (TP_ATR / SL_ATR, 2.5, 4.0) # TP1/TP2/TP3 (R cinsinden)
102
+
103
+ # --- v3 SERT FİLTRELER (gate) — düşük kaliteli sinyalleri eleyerek isabet oranını artırır ---
104
+ # Not: bu eşikler sinyal SAYISINI azaltır, isabet oranını yükseltmeye çalışır. %80 gibi bir
105
+ # oranın SÜREKLİ garanti edilebileceği bir gösterge kombinasyonu yoktur; bu filtreler sadece
106
+ # düşük güvenilirlikli barları "BEKLE/NÖTR"e çevirerek kalan sinyallerin ortalama kalitesini yükseltir.
107
+ MIN_REGIME_ADX = 12 # ADX bu değerin altında ve sıkışma yoksa -> belirgin trend yok, sinyal verilmez
108
+ MIN_AGREE_FOR_SIGNAL = 0.55 # faktör konsensüsü (agree) bu oranın altındaysa sinyal BEKLE'ye düşer
109
+ MTF_HARD_GATE = True # üst zaman dilimi (mtf1) net ters yöndeyse normal AL/SAT bile filtrelenir
110
+ OB_HARD_GATE_IMB = 0.35 # emir defteri dengesizliği sinyale bu kadar ters ve güçlüyse sinyal filtrelenir
111
+ STABILITY_MIN_SAMPLES = 12 # ilk-yarı/ikinci-yarı isabet stabilite kontrolü için min. sinyal sayısı
112
+ STABILITY_MAX_GAP = 0.30 # iki yarı arasındaki isabet farkı bunu aşarsa "tutarsız" (rejim kayması) işaretlenir
113
+
114
+
115
+ # ---------------------------------------------------------------------------
116
+ # OKX PUBLIC API WRAPPERS (önbellek + yeniden deneme)
117
+ # ---------------------------------------------------------------------------
118
+ _CACHE = {}
119
+ _CACHE_LOCK = threading.Lock()
120
+
121
+
122
+ def _cached(key, ttl, loader):
123
+ now = time.time()
124
+ with _CACHE_LOCK:
125
+ hit = _CACHE.get(key)
126
+ if hit and now - hit[0] < ttl:
127
+ return hit[1]
128
+ val = loader()
129
+ with _CACHE_LOCK:
130
+ _CACHE[key] = (time.time(), val)
131
+ if len(_CACHE) > 200: # eski kayıtları buda
132
+ for k in sorted(_CACHE, key=lambda x: _CACHE[x][0])[:60]:
133
+ _CACHE.pop(k, None)
134
+ return val
135
+
136
+
137
+ def _get(path, params=None, retries=2):
138
+ for attempt in range(retries + 1):
139
+ try:
140
+ r = requests.get(f"{OKX_BASE}{path}", params=params, timeout=TIMEOUT)
141
+ r.raise_for_status()
142
+ data = r.json()
143
+ code = str(data.get("code"))
144
+ if code == "50011" and attempt < retries: # OKX rate-limit
145
+ time.sleep(0.5 * (attempt + 1))
146
+ continue
147
+ if code != "0":
148
+ raise RuntimeError(data.get("msg") or "OKX API hata döndürdü.")
149
+ return data["data"]
150
+ except requests.exceptions.RequestException as e:
151
+ if attempt < retries:
152
+ time.sleep(0.3 * (attempt + 1))
153
+ continue
154
+ raise RuntimeError(f"Ağ hatası: {e}")
155
+
156
+
157
+ def fetch_candles(inst_id, bar="1H", limit=300):
158
+ def _load():
159
+ raw = _get("/api/v5/market/candles", {"instId": inst_id, "bar": bar, "limit": limit})
160
+ if not raw:
161
+ raise RuntimeError(f"{inst_id} için mum verisi bulunamadı (sembolü kontrol edin).")
162
+ cols = ["ts", "o", "h", "l", "c", "vol", "volCcy", "volCcyQuote", "confirm"]
163
+ df = pd.DataFrame(raw, columns=cols)
164
+ for c in ["o", "h", "l", "c", "vol"]:
165
+ df[c] = df[c].astype(float)
166
+ df["confirm"] = df["confirm"].astype(int)
167
+ df["ts"] = pd.to_datetime(df["ts"].astype(np.int64), unit="ms")
168
+ return df.sort_values("ts").reset_index(drop=True)
169
+
170
+ return _cached(("candles", inst_id, bar, limit), CACHE_TTL, _load).copy()
171
+
172
+
173
+ def fetch_ticker(inst_id):
174
+ raw = _get("/api/v5/market/ticker", {"instId": inst_id})
175
+ return raw[0] if raw else {}
176
+
177
+
178
+ def fetch_all_tickers(inst_type="SPOT"):
179
+ return _cached(("tickers", inst_type), 20, lambda: _get("/api/v5/market/tickers", {"instType": inst_type}))
180
+
181
+
182
+ def fetch_orderbook(inst_id, depth=20):
183
+ raw = _get("/api/v5/market/books", {"instId": inst_id, "sz": depth})
184
+ return raw[0] if raw else {}
185
+
186
+
187
+ def fetch_funding_rate(inst_id):
188
+ """Spot paritenin (BTC-USDT) sürekli vadeli karşılığının (BTC-USDT-SWAP) funding oranı."""
189
+ parts = inst_id.split("-")
190
+ if len(parts) != 2:
191
+ return None
192
+ raw = _get("/api/v5/public/funding-rate", {"instId": f"{inst_id}-SWAP"})
193
+ if not raw:
194
+ return None
195
+ return float(raw[0].get("fundingRate") or 0.0)
196
+
197
+
198
+ # ---------------------------------------------------------------------------
199
+ # TECHNICAL INDICATORS
200
+ # ---------------------------------------------------------------------------
201
+ def ema(s, p):
202
+ return s.ewm(span=p, adjust=False).mean()
203
+
204
+
205
+ def sma(s, p):
206
+ return s.rolling(p).mean()
207
+
208
+
209
+ def rsi(s, p=14):
210
+ d = s.diff()
211
+ gain = d.clip(lower=0)
212
+ loss = -d.clip(upper=0)
213
+ ag = gain.ewm(alpha=1 / p, adjust=False).mean()
214
+ al = loss.ewm(alpha=1 / p, adjust=False).mean()
215
+ rs = ag / al.replace(0, np.nan)
216
+ return (100 - 100 / (1 + rs)).fillna(50)
217
+
218
+
219
+ def macd(s, fast=12, slow=26, signal=9):
220
+ line = ema(s, fast) - ema(s, slow)
221
+ sig = ema(line, signal)
222
+ return line, sig, line - sig
223
+
224
+
225
+ def bollinger(s, p=20, k=2):
226
+ mid = sma(s, p)
227
+ std = s.rolling(p).std()
228
+ return mid + k * std, mid, mid - k * std
229
+
230
+
231
+ def atr(df, p=14):
232
+ h, l, c = df["h"], df["l"], df["c"]
233
+ pc = c.shift(1)
234
+ tr = pd.concat([h - l, (h - pc).abs(), (l - pc).abs()], axis=1).max(axis=1)
235
+ return tr.ewm(alpha=1 / p, adjust=False).mean()
236
+
237
+
238
+ def stochastic(df, k_p=14, d_p=3):
239
+ lo = df["l"].rolling(k_p).min()
240
+ hi = df["h"].rolling(k_p).max()
241
+ k = 100 * (df["c"] - lo) / (hi - lo).replace(0, np.nan)
242
+ d = k.rolling(d_p).mean()
243
+ return k.fillna(50), d.fillna(50)
244
+
245
+
246
+ def vwap_window(df):
247
+ tp = (df["h"] + df["l"] + df["c"]) / 3
248
+ return (tp * df["vol"]).cumsum() / df["vol"].cumsum().replace(0, np.nan)
249
+
250
+
251
+ def adx(df, p=14):
252
+ """ADX + yönsel indikatörler (+DI / -DI). Trend gücünü ölçer (0-100)."""
253
+ h, l, c = df["h"], df["l"], df["c"]
254
+ up_move = h.diff()
255
+ down_move = -l.diff()
256
+ plus_dm = np.where((up_move > down_move) & (up_move > 0), up_move, 0.0)
257
+ minus_dm = np.where((down_move > up_move) & (down_move > 0), down_move, 0.0)
258
+ tr = pd.concat([h - l, (h - c.shift(1)).abs(), (l - c.shift(1)).abs()], axis=1).max(axis=1)
259
+ atr_ = tr.ewm(alpha=1 / p, adjust=False).mean()
260
+ plus_di = 100 * pd.Series(plus_dm, index=df.index).ewm(alpha=1 / p, adjust=False).mean() / atr_.replace(0, np.nan)
261
+ minus_di = 100 * pd.Series(minus_dm, index=df.index).ewm(alpha=1 / p, adjust=False).mean() / atr_.replace(0, np.nan)
262
+ dx = ((plus_di - minus_di).abs() / (plus_di + minus_di).replace(0, np.nan)) * 100
263
+ adx_ = dx.ewm(alpha=1 / p, adjust=False).mean()
264
+ return adx_.fillna(0), plus_di.fillna(0), minus_di.fillna(0)
265
+
266
+
267
+ def obv(df):
268
+ """On-Balance Volume: hacmin fiyat yönüyle kümülatif toplamı."""
269
+ direction = np.sign(df["c"].diff()).fillna(0)
270
+ return (direction * df["vol"]).cumsum()
271
+
272
+
273
+ def supertrend(df, period=10, mult=3.0):
274
+ """Supertrend: ATR bantlı trend takipçisi. dir=+1 yükseliş, -1 düşüş. (nedensel, geleceğe bakmaz)"""
275
+ h, l, c = df["h"].to_numpy(), df["l"].to_numpy(), df["c"].to_numpy()
276
+ a = atr(df, period).to_numpy()
277
+ hl2 = (h + l) / 2
278
+ ub, lb = hl2 + mult * a, hl2 - mult * a
279
+ n = len(c)
280
+ fub, flb = ub.copy(), lb.copy()
281
+ direction = np.ones(n)
282
+ line = np.full(n, np.nan)
283
+ if n:
284
+ line[0] = flb[0]
285
+ for i in range(1, n):
286
+ fub[i] = ub[i] if (ub[i] < fub[i - 1] or c[i - 1] > fub[i - 1]) else fub[i - 1]
287
+ flb[i] = lb[i] if (lb[i] > flb[i - 1] or c[i - 1] < flb[i - 1]) else flb[i - 1]
288
+ if c[i] > fub[i - 1]:
289
+ direction[i] = 1
290
+ elif c[i] < flb[i - 1]:
291
+ direction[i] = -1
292
+ else:
293
+ direction[i] = direction[i - 1]
294
+ line[i] = flb[i] if direction[i] == 1 else fub[i]
295
+ return pd.Series(line, index=df.index), pd.Series(direction, index=df.index)
296
+
297
+
298
+ def ichimoku(df):
299
+ """Tenkan/Kijun + bulut. Bulut, 26 bar önce hesaplanan Senkou A/B'nin bugüne kaydırılmış halidir
300
+ (yani nedensel: gelecek veri kullanılmaz)."""
301
+ h, l = df["h"], df["l"]
302
+ tenkan = (h.rolling(9).max() + l.rolling(9).min()) / 2
303
+ kijun = (h.rolling(26).max() + l.rolling(26).min()) / 2
304
+ span_a = (tenkan + kijun) / 2
305
+ span_b = (h.rolling(52).max() + l.rolling(52).min()) / 2
306
+ return tenkan, kijun, span_a.shift(26), span_b.shift(26)
307
+
308
+
309
+ def mfi(df, p=14):
310
+ """Money Flow Index: hacim ağırlıklı RSI."""
311
+ tp = (df["h"] + df["l"] + df["c"]) / 3
312
+ rmf = tp * df["vol"]
313
+ d = tp.diff()
314
+ pos = rmf.where(d > 0, 0.0).rolling(p).sum()
315
+ neg = rmf.where(d < 0, 0.0).rolling(p).sum()
316
+ out = 100 - 100 / (1 + pos / neg.replace(0, np.nan))
317
+ out = out.where(~((neg == 0) & (pos > 0)), 100.0)
318
+ return out.fillna(50)
319
+
320
+
321
+ def cmf(df, p=20):
322
+ """Chaikin Money Flow: kapanışın bar aralığındaki konumuna göre hacim akışı (-1..+1)."""
323
+ rng = (df["h"] - df["l"]).replace(0, np.nan)
324
+ mfm = ((df["c"] - df["l"]) - (df["h"] - df["c"])) / rng
325
+ mfv = mfm.fillna(0) * df["vol"]
326
+ return (mfv.rolling(p).sum() / df["vol"].rolling(p).sum().replace(0, np.nan)).fillna(0)
327
+
328
+
329
+ def market_structure(df, w=10):
330
+ """+1: Higher-High & Higher-Low (yükseliş yapısı), -1: Lower-High & Lower-Low, 0: karışık."""
331
+ hi, lo = df["h"].rolling(w).max(), df["l"].rolling(w).min()
332
+ up = (hi > hi.shift(w)) & (lo > lo.shift(w))
333
+ dn = (hi < hi.shift(w)) & (lo < lo.shift(w))
334
+ return pd.Series(np.where(up, 1.0, np.where(dn, -1.0, 0.0)), index=df.index)
335
+
336
+
337
+ def squeeze_indicator(df, ind):
338
+ """TTM tarzı sıkışma: Bollinger bantları Keltner kanalının İÇİNE girerse volatilite sıkışmıştır.
339
+ Dönüş: (sıkışma_açık, son 3 barda sıkışma çözüldü mü, momentum)"""
340
+ kc_mid = ind["ema20"]
341
+ kc_u, kc_l = kc_mid + 1.5 * ind["atr"], kc_mid - 1.5 * ind["atr"]
342
+ on = (ind["bb_lower"] > kc_l) & (ind["bb_upper"] < kc_u)
343
+ release = (~on) & on.shift(1, fill_value=False)
344
+ rel3 = release.astype(float).rolling(3, min_periods=1).max()
345
+ hh, ll = df["h"].rolling(20).max(), df["l"].rolling(20).min()
346
+ mom = df["c"] - ((hh + ll) / 2 + sma(df["c"], 20)) / 2
347
+ return on.astype(float), rel3, mom
348
+
349
+
350
+ CANDLE_PATTERNS = [
351
+ ("pat_hammer", "Çekiç", 1),
352
+ ("pat_bull_eng", "Yükseliş Yutan Mum", 1),
353
+ ("pat_morning", "Sabah Yıldızı", 1),
354
+ ("pat_shooting", "Kayan Yıldız", -1),
355
+ ("pat_bear_eng", "Düşüş Yutan Mum", -1),
356
+ ("pat_evening", "Akşam Yıldızı", -1),
357
+ ]
358
+
359
+
360
+ def candle_patterns(df):
361
+ """Vektörel mum formasyonu tespiti (yalnızca geçmiş barlar kullanılır)."""
362
+ o, h, l, c = df["o"], df["h"], df["l"], df["c"]
363
+ body = (c - o).abs()
364
+ rng = (h - l).replace(0, np.nan)
365
+ upper = h - np.maximum(o, c)
366
+ lower = np.minimum(o, c) - l
367
+ avg_body = body.rolling(10, min_periods=3).mean()
368
+ prior_down = c.shift(1) < c.shift(4)
369
+ prior_up = c.shift(1) > c.shift(4)
370
+
371
+ hammer = (lower >= 0.6 * rng) & (body <= 0.3 * rng) & (upper <= 0.15 * rng) & prior_down
372
+ shooting = (upper >= 0.6 * rng) & (body <= 0.3 * rng) & (lower <= 0.15 * rng) & prior_up
373
+ bull_eng = ((c.shift(1) < o.shift(1)) & (c > o) & (c >= o.shift(1)) & (o <= c.shift(1))
374
+ & (body > body.shift(1)) & prior_down)
375
+ bear_eng = ((c.shift(1) > o.shift(1)) & (c < o) & (c <= o.shift(1)) & (o >= c.shift(1))
376
+ & (body > body.shift(1)) & prior_up)
377
+ big2 = body.shift(2) > avg_body.shift(2)
378
+ small1 = body.shift(1) < 0.35 * body.shift(2)
379
+ mid2 = (o.shift(2) + c.shift(2)) / 2
380
+ morning = (c.shift(2) < o.shift(2)) & big2 & small1 & (c > o) & (c > mid2)
381
+ evening = (c.shift(2) > o.shift(2)) & big2 & small1 & (c < o) & (c < mid2)
382
+
383
+ raw = {"pat_hammer": hammer, "pat_shooting": shooting, "pat_bull_eng": bull_eng,
384
+ "pat_bear_eng": bear_eng, "pat_morning": morning, "pat_evening": evening}
385
+ return {k: v.fillna(False).astype(float) for k, v in raw.items()}
386
+
387
+
388
+ def find_swings(series, window=3):
389
+ """Yerel tepe/dip (swing high/low) indekslerini bulur."""
390
+ n = len(series)
391
+ highs, lows = [], []
392
+ for i in range(window, n - window):
393
+ seg = series.iloc[i - window:i + window + 1]
394
+ if series.iloc[i] == seg.max():
395
+ highs.append(i)
396
+ if series.iloc[i] == seg.min():
397
+ lows.append(i)
398
+ return highs, lows
399
+
400
+
401
+ def detect_divergence(df, ind, lookback=60, window=3):
402
+ """Fiyat ile RSI arasındaki son swing'leri karşılaştırarak klasik uyumsuzluğu tespit eder."""
403
+ n = len(df)
404
+ lb = min(lookback, n)
405
+ close = df["c"].iloc[-lb:].reset_index(drop=True)
406
+ rsi_s = ind["rsi"].iloc[-lb:].reset_index(drop=True)
407
+ highs, lows = find_swings(close, window)
408
+ result = {"bullish": False, "bearish": False}
409
+ if len(highs) >= 2:
410
+ h1, h2 = highs[-2], highs[-1]
411
+ if close.iloc[h2] > close.iloc[h1] and rsi_s.iloc[h2] < rsi_s.iloc[h1]:
412
+ result["bearish"] = True
413
+ if len(lows) >= 2:
414
+ l1, l2 = lows[-2], lows[-1]
415
+ if close.iloc[l2] < close.iloc[l1] and rsi_s.iloc[l2] > rsi_s.iloc[l1]:
416
+ result["bullish"] = True
417
+ return result
418
+
419
+
420
+ def find_sr_levels(df, atr_v, window=5, lookback=200, max_levels=4):
421
+ """Swing tepe/diplerini kümeleyerek destek/direnç seviyeleri çıkarır.
422
+ Dönüş: (destekler, dirençler) — her biri [(seviye, temas_sayısı), ...], fiyata yakınlık sırasıyla."""
423
+ sub = df.iloc[-lookback:].reset_index(drop=True)
424
+ highs, _ = find_swings(sub["h"], window)
425
+ _, lows = find_swings(sub["l"], window)
426
+ pts = [float(sub["h"].iloc[i]) for i in highs] + [float(sub["l"].iloc[i]) for i in lows]
427
+ if not pts:
428
+ return [], []
429
+ price = float(df["c"].iloc[-1])
430
+ tol = max(0.6 * (atr_v if atr_v and atr_v > 0 else 0.0), price * 0.002)
431
+ pts.sort()
432
+ clusters, cur = [], [pts[0]]
433
+ for p in pts[1:]:
434
+ if p - float(np.mean(cur)) <= tol:
435
+ cur.append(p)
436
+ else:
437
+ clusters.append(cur)
438
+ cur = [p]
439
+ clusters.append(cur)
440
+ levels = [(float(np.mean(c)), len(c)) for c in clusters]
441
+ supports = sorted([lv for lv in levels if lv[0] < price], key=lambda x: -x[0])[:max_levels]
442
+ resists = sorted([lv for lv in levels if lv[0] > price], key=lambda x: x[0])[:max_levels]
443
+ return supports, resists
444
+
445
+
446
+ def compute_indicators(df):
447
+ close = df["c"]
448
+ ind = {}
449
+ ind["rsi"] = rsi(close)
450
+ ind["macd_line"], ind["macd_signal"], ind["macd_hist"] = macd(close)
451
+ ind["ema20"] = ema(close, 20)
452
+ ind["ema50"] = ema(close, 50)
453
+ ind["ema200"] = ema(close, 200 if len(close) >= 200 else max(5, len(close) // 2))
454
+ ind["bb_upper"], ind["bb_mid"], ind["bb_lower"] = bollinger(close)
455
+ ind["atr"] = atr(df)
456
+ ind["stoch_k"], ind["stoch_d"] = stochastic(df)
457
+ ind["vwap"] = vwap_window(df)
458
+ ind["adx"], ind["plus_di"], ind["minus_di"] = adx(df)
459
+ ind["obv"] = obv(df)
460
+ ind["obv_ema"] = ema(ind["obv"], 20)
461
+ # --- v2 ek faktörler ---
462
+ ind["st_line"], ind["st_dir"] = supertrend(df)
463
+ ind["tenkan"], ind["kijun"], ind["ich_a"], ind["ich_b"] = ichimoku(df)
464
+ ind["mfi"] = mfi(df)
465
+ ind["cmf"] = cmf(df)
466
+ ind["rvol"] = (df["vol"] / sma(df["vol"], 20).replace(0, np.nan)).fillna(1.0)
467
+ ind["dc_hi20"] = df["h"].rolling(20).max().shift(1)
468
+ ind["dc_lo20"] = df["l"].rolling(20).min().shift(1)
469
+ ind["struct"] = market_structure(df)
470
+ ind["sq_on"], ind["sq_rel3"], ind["sq_mom"] = squeeze_indicator(df, ind)
471
+ ind["atr_rank"] = (ind["atr"] / close).rolling(100, min_periods=30).apply(
472
+ lambda x: float((x <= x[-1]).mean()), raw=True).fillna(0.5)
473
+ for name, ser in candle_patterns(df).items():
474
+ ind[name] = ser
475
+ # hızlı, nedensel erişim için numpy önbelleği (backtest döngüsünde .iloc yerine kullanılır)
476
+ arr = {k: v.to_numpy(dtype=float) for k, v in ind.items()}
477
+ for k in ("o", "h", "l", "c", "vol"):
478
+ arr[k] = df[k].to_numpy(dtype=float)
479
+ ind["_arr"] = arr
480
+ return ind
481
+
482
+
483
+ # ---------------------------------------------------------------------------
484
+ # COMPOSITE SIGNAL ENGINE v2 (multi-factor, rejim-uyarlamalı, konsensüs filtreli)
485
+ # ---------------------------------------------------------------------------
486
+ def _score_at(ind, i, verbose=False):
487
+ """Çoklu faktör puanlamasını verilen bar indeksinde hesaplar (yalnızca o ana kadarki veri).
488
+
489
+ İki faktör ailesi vardır:
490
+ T (TREND) : MACD, EMA dizilimi, VWAP, OBV, Supertrend, Ichimoku, Donchian kırılımı,
491
+ piyasa yapısı, CMF, sıkışma kırılımı, ADX/DI yönü
492
+ R (REVERSION) : RSI, Bollinger, Stokastik, MFI, mum formasyonları
493
+
494
+ ADX'e göre iki ailenin ağırlığı SÜREKLİ olarak kayar (w: 0=yatay … 1=güçlü trend).
495
+ Güçlü trendde karşı-trend (aşırı alım/satım) sinyalleri bastırılır; yatay piyasada trend takibi bastırılır.
496
+ Sonuç, tanh ile ±10 aralığına yumuşak sıkıştırılır.
497
+ Canlı sinyal ve tarihsel backtest AYNI fonksiyonu kullanır (tutarlılık + gelecek sızıntısı yok)."""
498
+ A = ind["_arr"]
499
+ price = A["c"][i]
500
+ items = [] # (etiket, grup, ham_puan, açıklama)
501
+
502
+ def add(label, group, raw, text):
503
+ items.append((label, group, float(raw), text))
504
+
505
+ # --- RSI (R) ---
506
+ rsi_v = A["rsi"][i]
507
+ if rsi_v < 30:
508
+ add("RSI", "R", 2, f"RSI aşırı satım bölgesinde ({rsi_v:.1f})")
509
+ elif rsi_v > 70:
510
+ add("RSI", "R", -2, f"RSI aşırı alım bölgesinde ({rsi_v:.1f})")
511
+ else:
512
+ add("RSI", "R", 0, f"RSI nötr bölgede ({rsi_v:.1f})")
513
+
514
+ # --- MACD histogramı (T) ---
515
+ hist_v = A["macd_hist"][i]
516
+ hist_prev = A["macd_hist"][i - 1] if i > 0 else 0.0
517
+ if hist_v > 0 and hist_v > hist_prev:
518
+ add("MACD", "T", 1.5, "MACD histogramı pozitif ve güçleniyor")
519
+ elif hist_v > 0:
520
+ add("MACD", "T", 0.5, "MACD histogramı pozitif")
521
+ elif hist_v < 0 and hist_v < hist_prev:
522
+ add("MACD", "T", -1.5, "MACD histogramı negatif ve zayıflıyor")
523
+ else:
524
+ add("MACD", "T", -0.5, "MACD histogramı negatif")
525
+
526
+ # --- EMA dizilimi (T) ---
527
+ e20, e50, e200 = A["ema20"][i], A["ema50"][i], A["ema200"][i]
528
+ if price > e20 > e50 > e200:
529
+ add("EMA Dizilimi", "T", 2, "Güçlü yükseliş trendi (Fiyat > EMA20 > EMA50 > EMA200)")
530
+ elif price > e20 and e20 > e50:
531
+ add("EMA Dizilimi", "T", 1, "Yükseliş trendi (kısa vadeli ortalamaların üzerinde)")
532
+ elif price < e20 < e50 < e200:
533
+ add("EMA Dizilimi", "T", -2, "Güçlü düşüş trendi (Fiyat < EMA20 < EMA50 < EMA200)")
534
+ elif price < e20 and e20 < e50:
535
+ add("EMA Dizilimi", "T", -1, "Düşüş trendi (kısa vadeli ortalamaların altında)")
536
+ else:
537
+ add("EMA Dizilimi", "T", 0, "Trend yatay / belirsiz")
538
+
539
+ # --- Bollinger konumu (R) ---
540
+ bb_u, bb_l = A["bb_upper"][i], A["bb_lower"][i]
541
+ if bb_u == bb_u and bb_l == bb_l and bb_u != bb_l:
542
+ bb_pos = (price - bb_l) / (bb_u - bb_l)
543
+ if bb_pos < 0.15:
544
+ add("Bollinger", "R", 1, "Fiyat Bollinger alt bandına yakın (olası tepki)")
545
+ elif bb_pos > 0.85:
546
+ add("Bollinger", "R", -1, "Fiyat Bollinger üst bandına yakın (olası tepki)")
547
+
548
+ # --- Stokastik (R) ---
549
+ k_v = A["stoch_k"][i]
550
+ if k_v < 20:
551
+ add("Stokastik", "R", 1, f"Stokastik aşırı satım bölgesinde ({k_v:.1f})")
552
+ elif k_v > 80:
553
+ add("Stokastik", "R", -1, f"Stokastik aşırı alım bölgesinde ({k_v:.1f})")
554
+
555
+ # --- MFI (R) ---
556
+ mfi_v = A["mfi"][i]
557
+ if mfi_v < 20:
558
+ add("MFI", "R", 1, f"MFI (hacimli RSI) aşırı satımda ({mfi_v:.1f})")
559
+ elif mfi_v > 80:
560
+ add("MFI", "R", -1, f"MFI (hacimli RSI) aşırı alımda ({mfi_v:.1f})")
561
+
562
+ # --- Mum formasyonları (R) ---
563
+ hits = []
564
+ for key, name, sgn in CANDLE_PATTERNS:
565
+ if A[key][i] > 0:
566
+ hits.append((name, sgn))
567
+ if hits:
568
+ raw = max(-1.5, min(1.5, float(sum(s for _, s in hits))))
569
+ add("Mum Formasyonu", "R", raw, "Mum formasyonu: " + ", ".join(n for n, _ in hits))
570
+
571
+ # --- VWAP (T) ---
572
+ vwap_v = A["vwap"][i]
573
+ if vwap_v == vwap_v:
574
+ if price > vwap_v:
575
+ add("VWAP", "T", 0.5, "Fiyat pencere VWAP'ının üzerinde")
576
+ else:
577
+ add("VWAP", "T", -0.5, "Fiyat pencere VWAP'ının altında")
578
+
579
+ # --- OBV (T) ---
580
+ obv_v, obv_e = A["obv"][i], A["obv_ema"][i]
581
+ if obv_e == obv_e:
582
+ if obv_v > obv_e:
583
+ add("OBV", "T", 0.5, "OBV hacim akışı yükseliş yönünde")
584
+ else:
585
+ add("OBV", "T", -0.5, "OBV hacim akışı düşüş yönünde")
586
+
587
+ # --- CMF (T) ---
588
+ cmf_v = A["cmf"][i]
589
+ if cmf_v > 0.05:
590
+ add("CMF", "T", 0.5, f"Chaikin para akışı pozitif ({cmf_v:+.2f}) — alıcılar baskın")
591
+ elif cmf_v < -0.05:
592
+ add("CMF", "T", -0.5, f"Chaikin para akışı negatif ({cmf_v:+.2f}) — satıcılar baskın")
593
+
594
+ # --- Supertrend (T) ---
595
+ st_d = A["st_dir"][i]
596
+ st_flip = i > 0 and A["st_dir"][i] != A["st_dir"][i - 1]
597
+ if st_d > 0:
598
+ add("Supertrend", "T", 1.5 + (0.5 if st_flip else 0),
599
+ "Supertrend YÜKSELİŞ moduna döndü (taze sinyal)" if st_flip else "Supertrend yükseliş modunda")
600
+ else:
601
+ add("Supertrend", "T", -(1.5 + (0.5 if st_flip else 0)),
602
+ "Supertrend DÜŞÜŞ moduna döndü (taze sinyal)" if st_flip else "Supertrend düşüş modunda")
603
+
604
+ # --- Ichimoku (T) ---
605
+ sa, sb = A["ich_a"][i], A["ich_b"][i]
606
+ if sa == sa and sb == sb:
607
+ top, bot = max(sa, sb), min(sa, sb)
608
+ if price > top:
609
+ add("Ichimoku Bulutu", "T", 1.5, "Fiyat Ichimoku bulutunun üzerinde (boğa bölgesi)")
610
+ elif price < bot:
611
+ add("Ichimoku Bulutu", "T", -1.5, "Fiyat Ichimoku bulutunun altında (ayı bölgesi)")
612
+ else:
613
+ add("Ichimoku Bulutu", "T", 0, "Fiyat Ichimoku bulutunun içinde (kararsız bölge)")
614
+ tk, kj = A["tenkan"][i], A["kijun"][i]
615
+ if tk == tk and kj == kj:
616
+ add("Tenkan/Kijun", "T", 0.5 if tk > kj else -0.5,
617
+ "Tenkan > Kijun (kısa vadeli momentum yukarı)" if tk > kj else "Tenkan < Kijun (kısa vadeli momentum aşağı)")
618
+
619
+ # --- Donchian kırılımı (T) ---
620
+ dhi, dlo, rvol = A["dc_hi20"][i], A["dc_lo20"][i], A["rvol"][i]
621
+ if dhi == dhi and dlo == dlo:
622
+ if price > dhi:
623
+ if rvol >= 1.2:
624
+ add("Donchian Kırılımı", "T", 1.5, f"20 barlık tepe HACİMLİ kırıldı (göreli hacim {rvol:.1f}x)")
625
+ else:
626
+ add("Donchian Kırılımı", "T", 0.75, f"20 barlık tepe kırıldı ama hacim zayıf ({rvol:.1f}x)")
627
+ elif price < dlo:
628
+ if rvol >= 1.2:
629
+ add("Donchian Kırılımı", "T", -1.5, f"20 barlık dip HACİMLİ kırıldı (göreli hacim {rvol:.1f}x)")
630
+ else:
631
+ add("Donchian Kırılımı", "T", -0.75, f"20 barlık dip kırıldı ama hacim zayıf ({rvol:.1f}x)")
632
+
633
+ # --- Piyasa yapısı (T) ---
634
+ st_v = A["struct"][i]
635
+ if st_v > 0:
636
+ add("Piyasa Yapısı", "T", 1, "Piyasa yapısı: Higher-High / Higher-Low (yükseliş yapısı)")
637
+ elif st_v < 0:
638
+ add("Piyasa Yapısı", "T", -1, "Piyasa yapısı: Lower-High / Lower-Low (düşüş yapısı)")
639
+
640
+ # --- Sıkışma (Bollinger ⊂ Keltner) ---
641
+ squeeze_on = A["sq_on"][i] > 0
642
+ mom = A["sq_mom"][i]
643
+ if A["sq_rel3"][i] > 0 and mom == mom and mom != 0:
644
+ d = 1 if mom > 0 else -1
645
+ add("Sıkışma Kırılımı", "T", 1.5 * d,
646
+ "Volatilite sıkışması yukarı yönde çözüldü" if d > 0 else "Volatilite sıkışması aşağı yönde çözüldü")
647
+ elif squeeze_on:
648
+ add("Sıkışma", "T", 0, "Volatilite sıkışması aktif — kırılım bekleniyor, sinyal güveni düşürüldü")
649
+
650
+ # --- REJİM (ADX) — sürekli ağırlıklandırma ---
651
+ adx_v = A["adx"][i]
652
+ w = min(max((adx_v - 15.0) / 15.0, 0.0), 1.0) if adx_v == adx_v else 0.0 # 0 = yatay, 1 = güçlü trend
653
+ trend_w = 0.5 + 0.8 * w
654
+ rev_w = 1.3 - 0.95 * w
655
+ di_dir = 1 if A["plus_di"][i] > A["minus_di"][i] else -1
656
+ add("ADX / DI Yönü", "T", di_dir * w, "") # yönsel bonus; w kadar ölçeklenir
657
+ if w >= 0.67:
658
+ regime = "TREND"
659
+ add("Rejim", "T", 0, f"ADX güçlü trend gösteriyor ({adx_v:.1f}) — trend faktörleri ağırlıklı, karşı-trend sinyaller bastırıldı")
660
+ elif w <= 0.33:
661
+ regime = "YATAY"
662
+ add("Rejim", "T", 0, f"ADX zayıf/yatay piyasa ({adx_v:.1f}) — ortalamaya dönüş faktörleri ağırlıklı")
663
+ else:
664
+ regime = "GEÇİŞ"
665
+ add("Rejim", "T", 0, f"ADX geçiş bölgesinde ({adx_v:.1f}) — faktör ağırlıkları dengeli karışım")
666
+
667
+ contribs = [(lab, (trend_w if g == "T" else rev_w) * raw, raw) for lab, g, raw, _ in items]
668
+ raw_total = sum(c for _, c, _ in contribs)
669
+ if squeeze_on:
670
+ raw_total *= 0.75
671
+ score = 10.0 * math.tanh(raw_total / 9.0)
672
+ bull_w = sum(c for _, c, _ in contribs if c > 0)
673
+ bear_w = -sum(c for _, c, _ in contribs if c < 0)
674
+
675
+ out = {
676
+ "score": score, "price": price, "adx": adx_v, "w": w, "regime": regime,
677
+ "bull_w": bull_w, "bear_w": bear_w, "squeeze_on": bool(squeeze_on),
678
+ "atr_rank": A["atr_rank"][i], "pat_hits": hits,
679
+ }
680
+ if verbose:
681
+ def tone(raw):
682
+ return "up" if raw > 0 else ("down" if raw < 0 else "flat")
683
+ out["reasons"] = [(tone(raw), text) for _, _, raw, text in items if text]
684
+ out["contribs"] = [(lab, c) for lab, c, raw in contribs if abs(c) > 1e-9]
685
+ return out
686
+
687
+
688
+ def compute_score_series(ind, n):
689
+ """Son BACKTEST_LOOKBACK bar için (canlı bar dahil) skor serisi. Dönüş: (başlangıç_indeksi, [skor_dict,...])"""
690
+ start = min(max(WARMUP_BARS, n - BACKTEST_LOOKBACK), n - 1)
691
+ return start, [_score_at(ind, i) for i in range(start, n)]
692
+
693
+
694
+ # ---------------------------------------------------------------------------
695
+ # BACKTEST — yön doğruluğu + gerçek işlem simülasyonu (SL/TP/zaman çıkışı + komisyon)
696
+ # ---------------------------------------------------------------------------
697
+ def _factor_gate_ok(s):
698
+ """v3 sert filtre: bir skor barının 'kaliteli sinyal' sayılıp sayılmayacağını belirler.
699
+ Hem CANLI sinyalde hem BACKTEST'te AYNI fonksiyon kullanılır (tutarlılık, iyimser backtest yok).
700
+ İki koşul: (1) belirgin bir rejim var mı (ADX / sıkışma), (2) faktörler gerçekten aynı yönde mi
701
+ (konsensüs). Bu ikisi zayıfsa bar, isabet istatistiğine hiç dahil edilmez / işlem açılmaz."""
702
+ adx_v = s.get("adx")
703
+ if adx_v == adx_v and adx_v < MIN_REGIME_ADX and not s.get("squeeze_on"):
704
+ return False
705
+ tot = s["bull_w"] + s["bear_w"]
706
+ if tot <= 0:
707
+ return True
708
+ direction = 1 if s["score"] > 0 else -1
709
+ agree = (s["bull_w"] if direction > 0 else s["bear_w"]) / tot
710
+ return agree >= MIN_AGREE_FOR_SIGNAL
711
+
712
+
713
+ def directional_accuracy(df, start, scores, forward=5, threshold=SIGNAL_THRESHOLD):
714
+ """Klasik test: sinyal 'forward' bar sonra doğru yönü göstermiş mi? (rejim kırılımlı,
715
+ v3 sert filtresinden geçen barlar üzerinden). Ayrıca ilk-yarı / ikinci-yarı isabet oranını
716
+ karşılaştırarak rejim kayması ya da parametre aşırı-uyumu (overfit) belirtisi olup olmadığını işaretler."""
717
+ c = df["c"].to_numpy()
718
+ n = len(c)
719
+ stats = {"all": [0, 0], "trend": [0, 0], "range": [0, 0]}
720
+ signal_hits = []
721
+ for i in range(start, n - forward):
722
+ s = scores[i - start]
723
+ if abs(s["score"]) < threshold or not _factor_gate_ok(s):
724
+ continue
725
+ correct = (s["score"] > 0) == (c[i + forward] > c[i])
726
+ regime = "trend" if s["adx"] >= 25 else "range"
727
+ for key in ("all", regime):
728
+ stats[key][1] += 1
729
+ if correct:
730
+ stats[key][0] += 1
731
+ signal_hits.append(correct)
732
+ if stats["all"][1] < 5:
733
+ return None
734
+
735
+ def pack(key):
736
+ ok, t = stats[key]
737
+ return {"accuracy": ok / t, "n_signals": t} if t > 0 else None
738
+
739
+ half1_acc = half2_acc = None
740
+ stable = True
741
+ if len(signal_hits) >= STABILITY_MIN_SAMPLES:
742
+ mid = len(signal_hits) // 2
743
+ h1, h2 = signal_hits[:mid], signal_hits[mid:]
744
+ half1_acc = sum(h1) / len(h1)
745
+ half2_acc = sum(h2) / len(h2)
746
+ stable = abs(half1_acc - half2_acc) <= STABILITY_MAX_GAP
747
+
748
+ return {
749
+ "accuracy": stats["all"][0] / stats["all"][1], "correct": stats["all"][0],
750
+ "n_signals": stats["all"][1], "forward_bars": forward,
751
+ "trend_regime": pack("trend"), "range_regime": pack("range"),
752
+ "half1_acc": half1_acc, "half2_acc": half2_acc, "stable": stable,
753
+ }
754
+
755
+
756
+ def simulate_trades(df, ind, start, scores, threshold=SIGNAL_THRESHOLD):
757
+ """Sinyalleri sırayla (üst üste binmeden) işleme çevirir.
758
+ Giriş: sinyal barını izleyen barın AÇILIŞI (gerçekçi, ileri sızıntı yok).
759
+ Çıkış: SL (1.5×ATR) / TP (2.5×ATR) / 24 bar zaman çıkışı. Aynı barda ikisi de değerse SL öncelikli (kötümser)."""
760
+ A = ind["_arr"]
761
+ o, h, l, c, atr_a = A["o"], A["h"], A["l"], A["c"], A["atr"]
762
+ n = len(c)
763
+ trades = []
764
+ i = start
765
+ while i <= n - 2:
766
+ sd = scores[i - start]
767
+ s = sd["score"]
768
+ a = atr_a[i]
769
+ if abs(s) < threshold or not (a > 0) or not _factor_gate_ok(sd):
770
+ i += 1
771
+ continue
772
+ d = 1 if s > 0 else -1
773
+ entry = o[i + 1]
774
+ risk = SL_ATR * a
775
+ sl, tp = entry - d * risk, entry + d * TP_ATR * a
776
+ last = min(n - 1, i + 1 + MAX_HOLD_BARS)
777
+ exit_i, exit_px, why = None, None, None
778
+ for j in range(i + 1, last + 1):
779
+ hit_sl = (l[j] <= sl) if d > 0 else (h[j] >= sl)
780
+ hit_tp = (h[j] >= tp) if d > 0 else (l[j] <= tp)
781
+ if hit_sl:
782
+ exit_i, exit_px, why = j, sl, "SL"
783
+ break
784
+ if hit_tp:
785
+ exit_i, exit_px, why = j, tp, "TP"
786
+ break
787
+ if exit_i is None:
788
+ if last == i + 1 + MAX_HOLD_BARS:
789
+ exit_i, exit_px, why = last, c[last], "ZAMAN"
790
+ else:
791
+ break # veri bitti, işlem hâlâ açık — sonuçlandırılamaz
792
+ pnl = d * (exit_px - entry) / entry - 2 * FEE_PER_SIDE
793
+ r_mult = pnl / (risk / entry)
794
+ trades.append({"signal_i": i, "entry_i": i + 1, "exit_i": exit_i, "dir": d, "entry": entry,
795
+ "exit": exit_px, "pnl": pnl, "r": r_mult, "why": why, "score": s})
796
+ i = exit_i
797
+ return trades
798
+
799
+
800
+ def summarize_trades(trades, df, start):
801
+ if len(trades) < 3:
802
+ return None
803
+ n = len(df)
804
+ pnl = np.array([t["pnl"] for t in trades])
805
+ r = np.array([t["r"] for t in trades])
806
+ wins, losses = pnl[pnl > 0], pnl[pnl <= 0]
807
+ gross_loss = abs(losses.sum())
808
+ pf = float(wins.sum() / gross_loss) if gross_loss > 0 else (99.0 if wins.sum() > 0 else 0.0)
809
+ win_rate = len(wins) / len(pnl)
810
+ win_r, loss_r = r[r > 0], r[r <= 0]
811
+ avg_win_r = float(win_r.mean()) if len(win_r) else 0.0
812
+ avg_loss_r = float(abs(loss_r.mean())) if len(loss_r) else 0.0
813
+ kelly = None
814
+ if avg_win_r > 0 and avg_loss_r > 0:
815
+ kelly = win_rate - (1 - win_rate) / (avg_win_r / avg_loss_r)
816
+ sqn = float(np.sqrt(min(len(r), 100)) * r.mean() / r.std()) if r.std() > 0 else 0.0
817
+
818
+ mult = np.ones(n - start)
819
+ for t in trades:
820
+ mult[t["exit_i"] - start] *= (1 + t["pnl"])
821
+ equity = np.cumprod(mult)
822
+ peak = np.maximum.accumulate(equity)
823
+ max_dd = float(((equity - peak) / peak).min())
824
+ c = df["c"].to_numpy()
825
+ bh = c[start:] / c[start]
826
+
827
+ streak = best = 0
828
+ for p in pnl:
829
+ streak = streak + 1 if p <= 0 else 0
830
+ best = max(best, streak)
831
+
832
+ def side(dirn):
833
+ sub = [t for t in trades if t["dir"] == dirn]
834
+ return {"n": len(sub), "win_rate": (sum(1 for t in sub if t["pnl"] > 0) / len(sub)) if sub else None}
835
+
836
+ stats = {
837
+ "n": len(trades), "win_rate": win_rate, "profit_factor": pf, "expectancy": float(pnl.mean()),
838
+ "avg_r": float(r.mean()), "sqn": sqn, "max_dd": max_dd, "total_return": float(equity[-1] - 1),
839
+ "bh_return": float(bh[-1] - 1), "max_consec_loss": best, "avg_win_r": avg_win_r,
840
+ "avg_loss_r": avg_loss_r, "kelly": kelly, "long": side(1), "short": side(-1),
841
+ }
842
+ return stats, equity, bh
843
+
844
+
845
+ def run_backtest(df, ind, start, scores):
846
+ n = len(df)
847
+ if n - start < 20:
848
+ return None
849
+ acc = directional_accuracy(df, start, scores)
850
+ trades = simulate_trades(df, ind, start, scores)
851
+ summ = summarize_trades(trades, df, start)
852
+ bt = {"start": start, "acc": acc, "trades": trades, "stats": None, "equity": None, "bh": None}
853
+ if summ:
854
+ bt["stats"], bt["equity"], bt["bh"] = summ
855
+ return bt
856
+
857
+
858
+ # ---------------------------------------------------------------------------
859
+ # CANLI KATMAN — üst zaman dilimleri, emir defteri, funding (backtest EDİLMEZ)
860
+ # ---------------------------------------------------------------------------
861
+ def _mtf_score(symbol, bar, closed_only):
862
+ hdf = fetch_candles(symbol, bar, 300)
863
+ if closed_only:
864
+ hdf = hdf[hdf["confirm"] == 1].reset_index(drop=True)
865
+ hind = compute_indicators(hdf)
866
+ r = _score_at(hind, len(hdf) - 1)
867
+ return {"bar": bar, "score": r["score"]}
868
+
869
+
870
+ def _fetch_live_extras(symbol, bar, mtf, micro, closed_only):
871
+ """MTF / emir defteri / funding çağrılarını PARALEL yapar; hata veren parça sessizce atlanır."""
872
+ tasks = {}
873
+ with ThreadPoolExecutor(max_workers=4) as ex:
874
+ if mtf and symbol and bar:
875
+ b1 = MTF_MAP.get(bar)
876
+ if b1 and b1 != bar:
877
+ tasks["mtf1"] = ex.submit(_mtf_score, symbol, b1, closed_only)
878
+ b2 = MTF_MAP.get(b1)
879
+ if b2 and b2 not in (b1, bar):
880
+ tasks["mtf2"] = ex.submit(_mtf_score, symbol, b2, closed_only)
881
+ if micro and symbol:
882
+ tasks["ob"] = ex.submit(fetch_orderbook, symbol, 20)
883
+ tasks["fund"] = ex.submit(fetch_funding_rate, symbol)
884
+ out = {}
885
+ for k, f in tasks.items():
886
+ try:
887
+ out[k] = f.result()
888
+ except Exception:
889
+ out[k] = None
890
+ return out
891
+
892
+
893
+ def build_trade_plan(price, direction, atr_v, supports, resists, acct=None, risk_pct=None):
894
+ """Yapısal (destek/direnç) stop uygun aralıktaysa onu, değilse ATR stop'unu kullanır.
895
+ 3 kademeli hedef + hesap büyüklüğüne göre pozisyon boyutu."""
896
+ d = direction
897
+ stop, stop_type = price - d * SL_ATR * atr_v, f"ATR × {SL_ATR}"
898
+ cand = None
899
+ if d > 0 and supports:
900
+ cand = supports[0][0] - 0.25 * atr_v
901
+ elif d < 0 and resists:
902
+ cand = resists[0][0] + 0.25 * atr_v
903
+ if cand is not None:
904
+ dist = abs(price - cand)
905
+ if 0.8 * atr_v <= dist <= 3.0 * atr_v and (cand < price if d > 0 else cand > price):
906
+ stop, stop_type = cand, "Yapısal (S/R arkası)"
907
+ risk = abs(price - stop)
908
+ plan = {
909
+ "direction": "LONG" if d > 0 else "SHORT", "entry": price, "stop_loss": stop, "stop_type": stop_type,
910
+ "risk_dist": risk, "risk_frac": risk / price,
911
+ "targets": [(price + d * risk * m, m) for m in TP_R_MULTIPLES], "sizing": None,
912
+ }
913
+ if acct and acct > 0 and risk_pct and risk_pct > 0 and risk > 0:
914
+ risk_amt = acct * risk_pct / 100.0
915
+ qty = risk_amt / risk
916
+ notional = qty * price
917
+ plan["sizing"] = {"risk_amount": risk_amt, "qty": qty, "notional": notional, "leverage": notional / acct}
918
+ return plan
919
+
920
+
921
+ def compute_confidence(score, agree, mtf_state, hist_q, vol_q):
922
+ mag = min(abs(score) / 8.0, 1.0)
923
+ conf = 100.0 * (0.35 * mag + 0.25 * agree + 0.15 * mtf_state + 0.20 * hist_q + 0.05 * vol_q)
924
+ parts = {"Skor Büyüklüğü": mag, "Faktör Uyumu": agree, "MTF Teyidi": mtf_state,
925
+ "Tarihsel Kenar": hist_q, "Volatilite Rejimi": vol_q}
926
+ return min(conf, 100.0), parts
927
+
928
+
929
+ def grade_from_conf(conf):
930
+ for thr, g, col in ((75, "A+", "#3ddc97"), (62, "A", "#7fe6b8"), (50, "B", "#f2b84b"), (38, "C", "#ff9aa8")):
931
+ if conf >= thr:
932
+ return g, col
933
+ return "D", "#ff5c72"
934
+
935
+
936
+ def generate_signal(df, symbol=None, bar=None, mtf=True, micro=True, closed_only=False,
937
+ acct=None, risk_pct=None):
938
+ ind = compute_indicators(df)
939
+ n = len(df)
940
+ start, scores = compute_score_series(ind, n)
941
+ base = _score_at(ind, n - 1, verbose=True)
942
+ score, price, w = base["score"], base["price"], base["w"]
943
+ reasons, contribs = list(base["reasons"]), list(base["contribs"])
944
+ tags = []
945
+ if base["squeeze_on"]:
946
+ tags.append(("SIKIŞMA AKTİF", "#f2b84b"))
947
+ for name, sgn in base["pat_hits"]:
948
+ tags.append((name.upper(), "#3ddc97" if sgn > 0 else "#ff5c72"))
949
+
950
+ def overlay(label, delta, text):
951
+ nonlocal score
952
+ score += delta
953
+ reasons.append(("up" if delta > 0 else "down", text))
954
+ contribs.append((label, delta))
955
+
956
+ # --- Uyumsuzluk (RSI divergence) — reversion sinyali, rejime göre ağırlıklı ---
957
+ div = detect_divergence(df, ind)
958
+ div_weight = 1.5 - 1.1 * w # güçlü trendde 0.4, yatayda 1.5 (eski davranışla aynı uçlar)
959
+ if div["bullish"]:
960
+ overlay("Yükseliş Uyumsuzluğu", div_weight, "Yükseliş uyumsuzluğu tespit edildi (fiyat düşük dip, RSI yüksek dip)")
961
+ tags.append(("YÜKSELİŞ UYUMSUZLUĞU", "#3ddc97"))
962
+ if div["bearish"]:
963
+ overlay("Düşüş Uyumsuzluğu", -div_weight, "Düşüş uyumsuzluğu tespit edildi (fiyat yüksek tepe, RSI düşük tepe)")
964
+ tags.append(("DÜŞÜŞ UYUMSUZLUĞU", "#ff5c72"))
965
+
966
+ # --- Skor ivmesi ---
967
+ if n - 4 >= start:
968
+ momentum = base["score"] - scores[n - 4 - start]["score"]
969
+ if momentum > 1.5:
970
+ overlay("Skor İvmesi", 0.3, "Kompozit skor son barlarda hızla güçleniyor")
971
+ elif momentum < -1.5:
972
+ overlay("Skor İvmesi", -0.3, "Kompozit skor son barlarda hızla zayıflıyor")
973
+
974
+ # --- Canlı ekstralar (paralel çekim) ---
975
+ extras = _fetch_live_extras(symbol, bar, mtf, micro, closed_only)
976
+ sgn = 1 if score > 0 else (-1 if score < 0 else 0)
977
+
978
+ # --- 2 kademeli MTF teyidi ---
979
+ mtf_info, align_flags = [], []
980
+ for key, wt in (("mtf1", 1.0), ("mtf2", 0.6)):
981
+ m = extras.get(key)
982
+ if not m:
983
+ continue
984
+ hs = m["score"]
985
+ aligned = None
986
+ if abs(hs) >= 1.0 and sgn != 0:
987
+ aligned = bool(np.sign(hs) == sgn)
988
+ align_flags.append(aligned)
989
+ if aligned:
990
+ overlay(f"{m['bar']} Teyidi", sgn * wt, f"{m['bar']} zaman diliminde de aynı yönlü teyit var (skor {hs:+.1f})")
991
+ else:
992
+ overlay(f"{m['bar']} Çelişkisi", -sgn * wt, f"{m['bar']} zaman dilimi ters yönlü sinyal veriyor (skor {hs:+.1f}) — dikkat")
993
+ mtf_info.append({"bar": m["bar"], "score": round(hs, 2), "aligned": aligned})
994
+ mtf_state = (sum(1.0 for a in align_flags if a) / len(align_flags)) if align_flags else 0.5
995
+ mtf_opposed = any(a is False for a in align_flags)
996
+
997
+ # --- Emir defteri dengesizliği (derinlik ağırlıklı, kısa vadede anlamlı) ---
998
+ imb = None
999
+ ob = extras.get("ob")
1000
+ if ob and ob.get("bids") and ob.get("asks"):
1001
+ bw = sum(float(x[1]) / (k + 1) for k, x in enumerate(ob["bids"]))
1002
+ aw = sum(float(x[1]) / (k + 1) for k, x in enumerate(ob["asks"]))
1003
+ if bw + aw > 0:
1004
+ imb = (bw - aw) / (bw + aw)
1005
+ if abs(imb) >= 0.2:
1006
+ delta = max(-0.8, min(0.8, imb * 2)) * (0.5 if bar in SLOW_BARS else 1.0)
1007
+ overlay("Emir Defteri", delta,
1008
+ f"Emir defterinde {'alış' if imb > 0 else 'satış'} baskısı (dengesizlik {imb * 100:+.0f}%)")
1009
+
1010
+ # --- Funding rate (kalabalık pozisyon = kontraryen) ---
1011
+ fr = extras.get("fund")
1012
+ if fr is not None:
1013
+ if fr > 0.0005:
1014
+ overlay("Funding", -0.6, f"Funding aşırı pozitif ({fr * 100:.3f}%) — kalabalık long, tasfiye riski")
1015
+ elif fr > 0.0002:
1016
+ overlay("Funding", -0.3, f"Funding yüksek pozitif ({fr * 100:.3f}%) — long tarafı kalabalık")
1017
+ elif fr < -0.0005:
1018
+ overlay("Funding", 0.6, f"Funding aşırı negatif ({fr * 100:.3f}%) — kalabalık short, sıkışma potansiyeli")
1019
+ elif fr < -0.0002:
1020
+ overlay("Funding", 0.3, f"Funding negatif ({fr * 100:.3f}%) — short tarafı kalabalık")
1021
+
1022
+ # --- Destek / direnç yakınlığı ---
1023
+ atr_v = float(ind["atr"].iloc[-1])
1024
+ supports, resists = find_sr_levels(df, atr_v)
1025
+ sgn = 1 if score > 0 else (-1 if score < 0 else 0)
1026
+ if atr_v > 0 and sgn != 0:
1027
+ if sgn > 0:
1028
+ if resists and resists[0][1] >= 2 and (resists[0][0] - price) / atr_v <= 1.0:
1029
+ lv, tc = resists[0]
1030
+ overlay("Yakın Direnç", -0.8, f"Yakın direnç {fmt_price(lv)} ({tc} temas, {(lv - price) / atr_v:.1f} ATR) — yükselişi sınırlayabilir")
1031
+ elif supports and supports[0][1] >= 2 and (price - supports[0][0]) / atr_v <= 0.5:
1032
+ lv, tc = supports[0]
1033
+ overlay("Destek Üstünde", 0.4, f"Fiyat güçlü destek {fmt_price(lv)} ({tc} temas) üzerinde")
1034
+ else:
1035
+ if supports and supports[0][1] >= 2 and (price - supports[0][0]) / atr_v <= 1.0:
1036
+ lv, tc = supports[0]
1037
+ overlay("Yakın Destek", 0.8, f"Yakın destek {fmt_price(lv)} ({tc} temas, {(price - lv) / atr_v:.1f} ATR) — düşüşü sınırlayabilir")
1038
+ elif resists and resists[0][1] >= 2 and (resists[0][0] - price) / atr_v <= 0.5:
1039
+ lv, tc = resists[0]
1040
+ overlay("Direnç Altında", -0.4, f"Fiyat güçlü direnç {fmt_price(lv)} ({tc} temas) altında")
1041
+
1042
+ score = max(-10.0, min(10.0, score))
1043
+
1044
+ # --- Konsensüs + sınıflandırma ---
1045
+ direction = 1 if score > 0 else -1
1046
+ tot = base["bull_w"] + base["bear_w"]
1047
+ agree = ((base["bull_w"] if direction > 0 else base["bear_w"]) / tot) if tot > 0 else 0.5
1048
+ if abs(score) >= STRONG_THRESHOLD and agree >= 0.65 and not mtf_opposed:
1049
+ signal, color = ("GÜÇLÜ AL", "#3ddc97") if score > 0 else ("GÜÇLÜ SAT", "#ff5c72")
1050
+ elif abs(score) >= SIGNAL_THRESHOLD:
1051
+ signal, color = ("AL", "#7fe6b8") if score > 0 else ("SAT", "#ff9aa8")
1052
+ if abs(score) >= STRONG_THRESHOLD:
1053
+ reasons.append(("flat", f"Skor güçlü bölgede ancak konsensüs ({agree * 100:.0f}%) veya MTF filtresini geçemedi — GÜÇLÜ yerine normal sinyal verildi"))
1054
+ else:
1055
+ signal, color = "BEKLE / NÖTR", "#f2b84b"
1056
+
1057
+ # --- v3 SERT FİLTRELER — düşük kaliteli AL/SAT'ları BEKLE'ye çevirir (isabet oranını artırmak için) ---
1058
+ veto_reasons = []
1059
+ adx_v = base["adx"]
1060
+ if signal != "BEKLE / NÖTR":
1061
+ if adx_v == adx_v and adx_v < MIN_REGIME_ADX and not base["squeeze_on"]:
1062
+ veto_reasons.append(f"ADX çok düşük ({adx_v:.1f}) — belirgin bir trend/rejim yok, yön güvenilir değil")
1063
+ if agree < MIN_AGREE_FOR_SIGNAL:
1064
+ veto_reasons.append(f"Faktör konsensüsü zayıf (%{agree * 100:.0f}) — göstergeler birbiriyle çelişiyor")
1065
+ if MTF_HARD_GATE and align_flags and align_flags[0] is False and "GÜÇLÜ" not in signal:
1066
+ veto_reasons.append(f"Üst zaman dilimi ({mtf_info[0]['bar']}) net ters yönde — teyit yok")
1067
+ if imb is not None and np.sign(imb) not in (0, direction) and abs(imb) >= OB_HARD_GATE_IMB:
1068
+ veto_reasons.append(f"Emir defterinde sinyale güçlü ters yönde baskı var (dengesizlik {imb * 100:+.0f}%)")
1069
+ if veto_reasons:
1070
+ for vr in veto_reasons:
1071
+ reasons.append(("flat", f"FİLTRE: {vr}"))
1072
+ signal, color = "BEKLE / NÖTR", "#f2b84b"
1073
+ tags.append(("SERT FİLTRE İLE ELENDİ", "#7c8797"))
1074
+
1075
+ is_neutral = signal == "BEKLE / NÖTR"
1076
+
1077
+ # --- Tarihsel doğrulama ---
1078
+ backtest = run_backtest(df, ind, start, scores)
1079
+ hist_q = 0.4
1080
+ if backtest:
1081
+ qs = []
1082
+ acc = backtest["acc"]
1083
+ if acc:
1084
+ p_shrunk = (acc["correct"] + 5) / (acc["n_signals"] + 10) # Bayes büzülmesi (önsel %50)
1085
+ qs.append(min(max((p_shrunk - 0.45) / 0.20, 0.0), 1.0))
1086
+ if acc.get("stable") is False:
1087
+ qs.append(0.2) # ilk/ikinci yarı isabeti tutarsız -> rejim kayması / overfit riski
1088
+ reasons.append(("flat", f"Tarihsel isabet tutarsız: ilk yarı %{acc['half1_acc']*100:.0f} vs "
1089
+ f"ikinci yarı %{acc['half2_acc']*100:.0f} — bu kenara temkinli yaklaşın"))
1090
+ st = backtest["stats"]
1091
+ if st:
1092
+ qs.append(min(max((st["profit_factor"] - 0.8) / 0.8, 0.0), 1.0))
1093
+ if qs:
1094
+ hist_q = float(np.mean(qs))
1095
+ rank = base["atr_rank"]
1096
+ vol_q = 1.0 if 0.10 <= rank <= 0.90 else 0.5
1097
+ if rank > 0.90:
1098
+ reasons.append(("flat", f"Volatilite aşırı yüksek (ATR yüzdeliği %{rank * 100:.0f}) — stoplar geniş tutulmalı, güven düşürüldü"))
1099
+ conf, conf_parts = compute_confidence(score, agree, mtf_state, hist_q, vol_q)
1100
+ grade, grade_color = ("—", "#7c8797") if is_neutral else grade_from_conf(conf)
1101
+
1102
+ plan = None
1103
+ if not is_neutral and atr_v > 0:
1104
+ plan = build_trade_plan(price, direction, atr_v, supports, resists, acct, risk_pct)
1105
+
1106
+ return {
1107
+ "signal": signal, "score": round(score, 2), "confidence": round(conf, 1), "grade": grade,
1108
+ "grade_color": grade_color, "color": color, "reasons": reasons, "contribs": contribs,
1109
+ "indicators": ind, "price": price, "divergence": div, "mtf": mtf_info, "plan": plan,
1110
+ "backtest": backtest, "regime": base["regime"], "adx": base["adx"], "atr_rank": rank,
1111
+ "squeeze_on": base["squeeze_on"], "agree": agree, "bull_w": base["bull_w"], "bear_w": base["bear_w"],
1112
+ "conf_parts": conf_parts, "tags": tags, "funding": fr, "imbalance": imb,
1113
+ "supports": supports, "resists": resists, "atr": atr_v,
1114
+ }
1115
+
1116
+
1117
+ # ---------------------------------------------------------------------------
1118
+ # RISK ANALYTICS
1119
+ # ---------------------------------------------------------------------------
1120
+ def risk_metrics(df, bar):
1121
+ ret = df["c"].pct_change().dropna()
1122
+ if len(ret) < 5:
1123
+ return None
1124
+ periods = BAR_TO_PERIODS_PER_YEAR.get(bar, 365)
1125
+ vol_annual = ret.std() * np.sqrt(periods)
1126
+ mean_annual = ret.mean() * periods
1127
+ sharpe = mean_annual / vol_annual if vol_annual > 0 else 0.0
1128
+ cum = (1 + ret).cumprod()
1129
+ peak = cum.cummax()
1130
+ dd = (cum - peak) / peak
1131
+ var95 = np.percentile(ret, 5)
1132
+ tail = ret[ret <= var95]
1133
+ cvar95 = tail.mean() if len(tail) else var95
1134
+ return {
1135
+ "volatility_annual": vol_annual, "return_annual": mean_annual, "sharpe": sharpe,
1136
+ "max_drawdown": dd.min(), "current_dd": dd.iloc[-1],
1137
+ "var95_bar": var95, "cvar95_bar": cvar95,
1138
+ }
1139
+
1140
+
1141
+ # ---------------------------------------------------------------------------
1142
+ # FORMAT HELPERS
1143
+ # ---------------------------------------------------------------------------
1144
+ def _isnan(v):
1145
+ return v is None or (isinstance(v, float) and math.isnan(v))
1146
+
1147
+
1148
+ def fmt_price(v):
1149
+ if _isnan(v):
1150
+ return "—"
1151
+ av = abs(v)
1152
+ if av >= 1000:
1153
+ return f"{v:,.2f}"
1154
+ if av >= 1:
1155
+ return f"{v:,.4f}"
1156
+ if av >= 0.01:
1157
+ return f"{v:.6f}"
1158
+ return f"{v:.8f}"
1159
+
1160
+
1161
+ def fmt_pct(v):
1162
+ if _isnan(v):
1163
+ return "—"
1164
+ sign = "+" if v > 0 else ""
1165
+ return f"{sign}{v * 100:.2f}%"
1166
+
1167
+
1168
+ def fmt_large(v):
1169
+ if _isnan(v):
1170
+ return "—"
1171
+ av = abs(v)
1172
+ if av >= 1e9:
1173
+ return f"{v / 1e9:.2f}B"
1174
+ if av >= 1e6:
1175
+ return f"{v / 1e6:.2f}M"
1176
+ if av >= 1e3:
1177
+ return f"{v / 1e3:.2f}K"
1178
+ return f"{v:.2f}"
1179
+
1180
+
1181
+ def metric_card(label, value, sub="", tone="neutral"):
1182
+ tone_class = {"up": "mc-up", "down": "mc-down", "neutral": "mc-neutral"}.get(tone, "mc-neutral")
1183
+ sub_html = f'<div class="mc-sub">{sub}</div>' if sub else ""
1184
+ return (f'<div class="metric-card {tone_class}"><div class="mc-label">{label}</div>'
1185
+ f'<div class="mc-value">{value}</div>{sub_html}</div>')
1186
+
1187
+
1188
+ def section(title):
1189
+ return f'<div class="section-title">{title}</div>'
1190
+
1191
+
1192
+ def cards_row(cards):
1193
+ return f'<div class="metric-row">{"".join(cards)}</div>' if cards else ""
1194
+
1195
+
1196
+ def error_box(msg):
1197
+ return f'<div class="error-box">⚠ {msg}</div>'
1198
+
1199
+
1200
+ # ---------------------------------------------------------------------------
1201
+ # CHART BUILDERS
1202
+ # ---------------------------------------------------------------------------
1203
+ def _base_layout(fig, title, height):
1204
+ fig.update_layout(
1205
+ template="plotly_dark", height=height, margin=dict(l=10, r=10, t=40, b=10),
1206
+ paper_bgcolor=CHART_BG, plot_bgcolor=CHART_BG, title=title,
1207
+ legend=dict(orientation="h", y=1.05, font=dict(size=10)),
1208
+ font=dict(family="IBM Plex Mono, monospace", size=11),
1209
+ )
1210
+ return fig
1211
+
1212
+
1213
+ def _empty_fig(msg, title="", height=220):
1214
+ fig = go.Figure()
1215
+ fig.add_annotation(text=msg, x=0.5, y=0.5, xref="paper", yref="paper", showarrow=False,
1216
+ font=dict(size=13, color="#7c8797"))
1217
+ fig.update_xaxes(visible=False)
1218
+ fig.update_yaxes(visible=False)
1219
+ return _base_layout(fig, title, height)
1220
+
1221
+
1222
+ def build_price_chart(df, ind, symbol, bar):
1223
+ fig = make_subplots(rows=2, cols=1, shared_xaxes=True, row_heights=[0.75, 0.25], vertical_spacing=0.03)
1224
+ fig.add_trace(go.Candlestick(
1225
+ x=df["ts"], open=df["o"], high=df["h"], low=df["l"], close=df["c"], name=symbol,
1226
+ increasing_line_color="#3ddc97", decreasing_line_color="#ff5c72"), row=1, col=1)
1227
+ fig.add_trace(go.Scatter(x=df["ts"], y=ind["ema20"], name="EMA20",
1228
+ line=dict(color="#5b8cff", width=1)), row=1, col=1)
1229
+ fig.add_trace(go.Scatter(x=df["ts"], y=ind["ema50"], name="EMA50",
1230
+ line=dict(color="#f2b84b", width=1)), row=1, col=1)
1231
+ fig.add_trace(go.Scatter(x=df["ts"], y=ind["bb_upper"], name="BB Üst",
1232
+ line=dict(color="rgba(150,160,255,0.45)", width=1, dash="dot")), row=1, col=1)
1233
+ fig.add_trace(go.Scatter(x=df["ts"], y=ind["bb_lower"], name="BB Alt",
1234
+ line=dict(color="rgba(150,160,255,0.45)", width=1, dash="dot"),
1235
+ fill="tonexty", fillcolor="rgba(150,160,255,0.05)"), row=1, col=1)
1236
+ vol_colors = np.where(df["c"] >= df["o"], "#3ddc97", "#ff5c72")
1237
+ fig.add_trace(go.Bar(x=df["ts"], y=df["vol"], name="Hacim", marker_color=vol_colors, opacity=0.55),
1238
+ row=2, col=1)
1239
+ fig.update_layout(xaxis_rangeslider_visible=False)
1240
+ return _base_layout(fig, f"{symbol} · {bar}", 520)
1241
+
1242
+
1243
+ def build_signal_price_chart(df, ind, symbol, bar, res):
1244
+ """Sinyal sekmesinin ana grafiği: mum + Supertrend + Ichimoku bulutu + S/R + geçmiş sinyal işaretleri + işlem planı."""
1245
+ n = len(df)
1246
+ v0 = max(0, n - 200)
1247
+ sl = slice(v0, n)
1248
+ ts = df["ts"].iloc[sl]
1249
+ fig = go.Figure()
1250
+ fig.add_trace(go.Candlestick(
1251
+ x=ts, open=df["o"].iloc[sl], high=df["h"].iloc[sl], low=df["l"].iloc[sl], close=df["c"].iloc[sl],
1252
+ name=symbol, increasing_line_color="#3ddc97", decreasing_line_color="#ff5c72"))
1253
+ # Ichimoku bulutu
1254
+ fig.add_trace(go.Scatter(x=ts, y=ind["ich_a"].iloc[sl], line=dict(width=0), showlegend=False, hoverinfo="skip"))
1255
+ fig.add_trace(go.Scatter(x=ts, y=ind["ich_b"].iloc[sl], line=dict(width=0), name="Ichimoku Bulutu",
1256
+ fill="tonexty", fillcolor="rgba(120,140,255,0.10)", hoverinfo="skip"))
1257
+ # Supertrend (yükseliş/düşüş ayrı renk)
1258
+ st_line, st_dir = ind["st_line"].iloc[sl], ind["st_dir"].iloc[sl]
1259
+ fig.add_trace(go.Scatter(x=ts, y=st_line.where(st_dir > 0), name="Supertrend ▲",
1260
+ line=dict(color="#3ddc97", width=1.5), connectgaps=False))
1261
+ fig.add_trace(go.Scatter(x=ts, y=st_line.where(st_dir < 0), name="Supertrend ▼",
1262
+ line=dict(color="#ff5c72", width=1.5), connectgaps=False))
1263
+ # Destek / direnç
1264
+ for lv, tc in res["supports"][:3]:
1265
+ fig.add_hline(y=lv, line=dict(color="rgba(61,220,151,0.55)", dash="dash", width=1),
1266
+ annotation_text=f"D {fmt_price(lv)} ×{tc}", annotation_position="left",
1267
+ annotation_font=dict(size=9, color="#3ddc97"))
1268
+ for lv, tc in res["resists"][:3]:
1269
+ fig.add_hline(y=lv, line=dict(color="rgba(255,92,114,0.55)", dash="dash", width=1),
1270
+ annotation_text=f"R {fmt_price(lv)} ×{tc}", annotation_position="left",
1271
+ annotation_font=dict(size=9, color="#ff5c72"))
1272
+ # Geçmiş sinyal işlemleri
1273
+ bt = res["backtest"]
1274
+ if bt and bt["trades"]:
1275
+ atr_a = ind["_arr"]["atr"]
1276
+ for d, sym_, col, nm in ((1, "triangle-up", "#3ddc97", "Geçmiş LONG"), (-1, "triangle-down", "#ff5c72", "Geçmiş SHORT")):
1277
+ tr = [t for t in bt["trades"] if t["dir"] == d and t["entry_i"] >= v0]
1278
+ if not tr:
1279
+ continue
1280
+ xs = [df["ts"].iloc[t["entry_i"]] for t in tr]
1281
+ ys = [(df["l"].iloc[t["entry_i"]] - 0.6 * atr_a[t["entry_i"]]) if d > 0
1282
+ else (df["h"].iloc[t["entry_i"]] + 0.6 * atr_a[t["entry_i"]]) for t in tr]
1283
+ hov = [f"{nm} · skor {t['score']:+.1f}<br>Sonuç: {t['why']} ({t['pnl'] * 100:+.2f}% · {t['r']:+.2f}R)" for t in tr]
1284
+ fig.add_trace(go.Scatter(x=xs, y=ys, mode="markers", name=nm, text=hov, hoverinfo="text",
1285
+ marker=dict(symbol=sym_, size=9, color=col, line=dict(width=0.5, color="#0a0e14"))))
1286
+ # Güncel işlem planı
1287
+ plan = res["plan"]
1288
+ if plan:
1289
+ x0, x1 = df["ts"].iloc[max(0, n - 30)], df["ts"].iloc[-1]
1290
+ lines = [("GİRİŞ", plan["entry"], "#e6ebf2"), ("SL", plan["stop_loss"], "#ff5c72")]
1291
+ lines += [(f"TP{k + 1}", px, "#3ddc97") for k, (px, _) in enumerate(plan["targets"])]
1292
+ for lab, y, col in lines:
1293
+ fig.add_shape(type="line", x0=x0, x1=x1, y0=y, y1=y, line=dict(color=col, width=1.4, dash="dot"))
1294
+ fig.add_annotation(x=x1, y=y, text=f"{lab} {fmt_price(y)}", showarrow=False, xanchor="left",
1295
+ font=dict(size=9, color=col))
1296
+ fig.update_layout(xaxis_rangeslider_visible=False)
1297
+ return _base_layout(fig, f"{symbol} · {bar} · Sinyal Haritası", 560)
1298
+
1299
+
1300
+ def build_factor_chart(contribs, symbol):
1301
+ items = [(l, v) for l, v in contribs if abs(v) > 1e-9]
1302
+ if not items:
1303
+ return _empty_fig("Faktör verisi yok", f"{symbol} · Faktör Katkıları")
1304
+ items.sort(key=lambda x: x[1])
1305
+ labels, vals = [l for l, _ in items], [v for _, v in items]
1306
+ fig = go.Figure(go.Bar(
1307
+ x=vals, y=labels, orientation="h", marker_color=["#3ddc97" if v > 0 else "#ff5c72" for v in vals],
1308
+ text=[f"{v:+.2f}" for v in vals], textposition="outside", cliponaxis=False))
1309
+ fig.update_layout(showlegend=False, xaxis_title="Ağırlıklı katkı (rejim ağırlığı uygulanmış)")
1310
+ return _base_layout(fig, f"{symbol} · Faktör Katkıları (skoru ne yönlendiriyor?)", max(300, 25 * len(items) + 100))
1311
+
1312
+
1313
+ def build_backtest_chart(bt, df, symbol):
1314
+ if not bt or not bt.get("stats"):
1315
+ return _empty_fig("Backtest için yeterli işlem oluşmadı (en az 3 işlem gerekir)", f"{symbol} · Sinyal Backtesti")
1316
+ start = bt["start"]
1317
+ ts = df["ts"].iloc[start:]
1318
+ fig = make_subplots(rows=2, cols=1, row_heights=[0.62, 0.38], vertical_spacing=0.14,
1319
+ subplot_titles=("Sinyal Stratejisi vs Al&Tut (komisyon dahil, tam pozisyon)", "İşlem Bazında R Çarpanı"))
1320
+ fig.add_trace(go.Scatter(x=ts, y=bt["equity"], name="Sinyal Stratejisi", line=dict(color="#5b8cff", width=2, shape="hv")),
1321
+ row=1, col=1)
1322
+ fig.add_trace(go.Scatter(x=ts, y=bt["bh"], name="Al ve Tut", line=dict(color="#7c8797", width=1.2)), row=1, col=1)
1323
+ rs = [t["r"] for t in bt["trades"]]
1324
+ fig.add_trace(go.Bar(x=list(range(1, len(rs) + 1)), y=rs, name="R",
1325
+ marker_color=["#3ddc97" if r > 0 else "#ff5c72" for r in rs],
1326
+ text=[t["why"] for t in bt["trades"]], hoverinfo="text+y"), row=2, col=1)
1327
+ fig.update_xaxes(title_text="İşlem #", row=2, col=1)
1328
+ return _base_layout(fig, f"{symbol} · Walk-Forward İşlem Simülasyonu", 470)
1329
+
1330
+
1331
+ def build_signal_chart(df, ind, symbol):
1332
+ fig = make_subplots(rows=4, cols=1, shared_xaxes=True, row_heights=[0.27, 0.25, 0.24, 0.24],
1333
+ vertical_spacing=0.05,
1334
+ subplot_titles=("RSI (14)", "MACD (12,26,9)", "Stokastik (14,3)", "ADX / DI (14)"))
1335
+ fig.add_trace(go.Scatter(x=df["ts"], y=ind["rsi"], name="RSI", line=dict(color="#5b8cff")), row=1, col=1)
1336
+ fig.add_hline(y=70, line=dict(color="#ff5c72", dash="dot", width=1), row=1, col=1)
1337
+ fig.add_hline(y=30, line=dict(color="#3ddc97", dash="dot", width=1), row=1, col=1)
1338
+
1339
+ hist_colors = np.where(ind["macd_hist"] >= 0, "#3ddc97", "#ff5c72")
1340
+ fig.add_trace(go.Bar(x=df["ts"], y=ind["macd_hist"], name="Histogram", marker_color=hist_colors), row=2, col=1)
1341
+ fig.add_trace(go.Scatter(x=df["ts"], y=ind["macd_line"], name="MACD",
1342
+ line=dict(color="#f2b84b", width=1)), row=2, col=1)
1343
+ fig.add_trace(go.Scatter(x=df["ts"], y=ind["macd_signal"], name="Sinyal",
1344
+ line=dict(color="#5b8cff", width=1)), row=2, col=1)
1345
+
1346
+ fig.add_trace(go.Scatter(x=df["ts"], y=ind["stoch_k"], name="%K", line=dict(color="#5b8cff")), row=3, col=1)
1347
+ fig.add_trace(go.Scatter(x=df["ts"], y=ind["stoch_d"], name="%D", line=dict(color="#f2b84b")), row=3, col=1)
1348
+ fig.add_hline(y=80, line=dict(color="#ff5c72", dash="dot", width=1), row=3, col=1)
1349
+ fig.add_hline(y=20, line=dict(color="#3ddc97", dash="dot", width=1), row=3, col=1)
1350
+
1351
+ fig.add_trace(go.Scatter(x=df["ts"], y=ind["adx"], name="ADX", line=dict(color="#e6ebf2", width=1.5)), row=4, col=1)
1352
+ fig.add_trace(go.Scatter(x=df["ts"], y=ind["plus_di"], name="+DI", line=dict(color="#3ddc97", width=1)), row=4, col=1)
1353
+ fig.add_trace(go.Scatter(x=df["ts"], y=ind["minus_di"], name="-DI", line=dict(color="#ff5c72", width=1)), row=4, col=1)
1354
+ fig.add_hline(y=25, line=dict(color="#f2b84b", dash="dot", width=1), row=4, col=1)
1355
+
1356
+ return _base_layout(fig, f"{symbol} · Momentum Göstergeleri", 720)
1357
+
1358
+
1359
+ def build_equity_chart(df, symbol):
1360
+ ret = df["c"].pct_change().fillna(0)
1361
+ cum = (1 + ret).cumprod()
1362
+ peak = cum.cummax()
1363
+ dd = (cum - peak) / peak
1364
+ fig = make_subplots(rows=2, cols=1, shared_xaxes=True, row_heights=[0.65, 0.35], vertical_spacing=0.06,
1365
+ subplot_titles=("Kümülatif Getiri (Al ve Tut)", "Drawdown"))
1366
+ fig.add_trace(go.Scatter(x=df["ts"], y=cum, name="Kümülatif Getiri", line=dict(color="#5b8cff"),
1367
+ fill="tozeroy", fillcolor="rgba(91,140,255,0.08)"), row=1, col=1)
1368
+ fig.add_trace(go.Scatter(x=df["ts"], y=dd, name="Drawdown", line=dict(color="#ff5c72"),
1369
+ fill="tozeroy", fillcolor="rgba(255,92,114,0.12)"), row=2, col=1)
1370
+ fig.update_layout(showlegend=False)
1371
+ return _base_layout(fig, f"{symbol} · Risk Profili", 440)
1372
+
1373
+
1374
+ def build_depth_chart(bids, asks, symbol):
1375
+ bids_sorted = sorted(bids, key=lambda x: -x[0])
1376
+ asks_sorted = sorted(asks, key=lambda x: x[0])
1377
+ bid_prices = [p for p, _ in bids_sorted]
1378
+ bid_cum = np.cumsum([s for _, s in bids_sorted])
1379
+ ask_prices = [p for p, _ in asks_sorted]
1380
+ ask_cum = np.cumsum([s for _, s in asks_sorted])
1381
+ fig = go.Figure()
1382
+ fig.add_trace(go.Scatter(x=bid_prices, y=bid_cum, name="Alış (Bid)", line=dict(color="#3ddc97", width=2),
1383
+ fill="tozeroy", fillcolor="rgba(61,220,151,0.15)"))
1384
+ fig.add_trace(go.Scatter(x=ask_prices, y=ask_cum, name="Satış (Ask)", line=dict(color="#ff5c72", width=2),
1385
+ fill="tozeroy", fillcolor="rgba(255,92,114,0.15)"))
1386
+ fig.update_layout(xaxis_title="Fiyat", yaxis_title="Kümülatif Miktar")
1387
+ return _base_layout(fig, f"{symbol} · Emir Defteri Derinliği", 380)
1388
+
1389
+
1390
+ # ---------------------------------------------------------------------------
1391
+ # TAB CALLBACKS
1392
+ # ---------------------------------------------------------------------------
1393
+ def load_overview(symbol, bar):
1394
+ try:
1395
+ df = fetch_candles(symbol, bar, 300)
1396
+ ticker = fetch_ticker(symbol)
1397
+ ind = compute_indicators(df)
1398
+ last = float(ticker.get("last") or df["c"].iloc[-1])
1399
+ open24h = float(ticker.get("open24h") or df["c"].iloc[0])
1400
+ high24h = float(ticker.get("high24h") or df["h"].max())
1401
+ low24h = float(ticker.get("low24h") or df["l"].min())
1402
+ vol24h = float(ticker.get("volCcy24h") or df["vol"].sum())
1403
+ chg = (last - open24h) / open24h if open24h else 0.0
1404
+ tone = "up" if chg >= 0 else "down"
1405
+ cards = [
1406
+ metric_card("SON FİYAT", f"${fmt_price(last)}", "", tone),
1407
+ metric_card("24S DEĞİŞİM", fmt_pct(chg), "", tone),
1408
+ metric_card("24S YÜKSEK", f"${fmt_price(high24h)}"),
1409
+ metric_card("24S DÜŞÜK", f"${fmt_price(low24h)}"),
1410
+ metric_card("24S HACİM (USDT)", f"${fmt_large(vol24h)}"),
1411
+ ]
1412
+ return cards_row(cards), build_price_chart(df, ind, symbol, bar)
1413
+ except Exception as e:
1414
+ return error_box(f"Veri alınamadı: {e}"), go.Figure()
1415
+
1416
+
1417
+ def _reasons_html(reasons):
1418
+ icon = {"up": "▲", "down": "▼", "flat": "•"}
1419
+ return "".join(f'<li class="r-{t}"><span class="ri">{icon[t]}</span>{txt}</li>' for t, txt in reasons)
1420
+
1421
+
1422
+ def load_signal(symbol, bar, closed_only=False, acct=10000, risk_pct=1.0):
1423
+ empty5 = lambda html: (html, go.Figure(), go.Figure(), go.Figure(), go.Figure())
1424
+ try:
1425
+ df = fetch_candles(symbol, bar, 300)
1426
+ if closed_only:
1427
+ df = df[df["confirm"] == 1].reset_index(drop=True)
1428
+ if len(df) < 30:
1429
+ return empty5(error_box("Sinyal için yeterli mum verisi yok (en az 30 bar gerekir)."))
1430
+ acct = float(acct or 0)
1431
+ risk_pct = float(risk_pct or 0)
1432
+ res = generate_signal(df, symbol=symbol, bar=bar, mtf=True, micro=True, closed_only=closed_only,
1433
+ acct=acct, risk_pct=risk_pct)
1434
+ ind, plan, bt = res["indicators"], res["plan"], res["backtest"]
1435
+
1436
+ # ---- Ana panel ----
1437
+ regime_col = {"TREND": "#5b8cff", "YATAY": "#f2b84b", "GEÇİŞ": "#7c8797"}[res["regime"]]
1438
+ panel = (
1439
+ f'<div class="signal-panel">'
1440
+ f'<div class="signal-badge" style="--tone:{res["color"]}">{res["signal"]}</div>'
1441
+ f'<div class="signal-meta"><div class="mc-label">KOMPOZİT SKOR</div>'
1442
+ f'<div class="mc-value">{res["score"]:+.2f} / ±10</div></div>'
1443
+ f'<div class="signal-meta"><div class="mc-label">GÜVEN</div>'
1444
+ f'<div class="mc-value">%{res["confidence"]}</div></div>'
1445
+ f'<div class="signal-meta"><div class="mc-label">KALİTE NOTU</div>'
1446
+ f'<div class="mc-value" style="color:{res["grade_color"]}">{res["grade"]}</div></div>'
1447
+ f'<div class="signal-meta"><div class="mc-label">REJİM</div>'
1448
+ f'<div class="mc-value" style="color:{regime_col}">{res["regime"]}</div></div>'
1449
+ f'</div>'
1450
+ )
1451
+ tot = res["bull_w"] + res["bear_w"]
1452
+ bull_pct = (res["bull_w"] / tot * 100) if tot > 0 else 50.0
1453
+ consensus = (
1454
+ f'<div class="consensus"><div class="mc-label">FAKTÖR KONSENSÜSÜ · BOĞA %{bull_pct:.0f} / AYI %{100 - bull_pct:.0f}</div>'
1455
+ f'<div class="cbar"><div class="cbull" style="width:{bull_pct:.1f}%"></div>'
1456
+ f'<div class="cbear" style="width:{100 - bull_pct:.1f}%"></div></div></div>'
1457
+ )
1458
+ conf_tags = "".join(
1459
+ f'<div class="tag" style="color:var(--text-muted)">{k} %{v * 100:.0f}</div>' for k, v in res["conf_parts"].items())
1460
+ tag_html = ""
1461
+ if res["tags"]:
1462
+ tag_html = '<div class="tags" style="margin-bottom:12px;">' + "".join(
1463
+ f'<div class="tag" style="border-color:{c};color:{c};">{t}</div>' for t, c in res["tags"]) + "</div>"
1464
+ conf_html = f'<div class="tags" style="margin-bottom:12px;">{conf_tags}</div>'
1465
+
1466
+ # ---- İşlem planı + pozisyon boyutu ----
1467
+ plan_cards, size_cards = [], []
1468
+ if plan:
1469
+ up = plan["direction"] == "LONG"
1470
+ plan_cards = [
1471
+ metric_card("ÖNERİLEN YÖN", plan["direction"], "", "up" if up else "down"),
1472
+ metric_card("GİRİŞ", f"${fmt_price(plan['entry'])}"),
1473
+ metric_card("STOP-LOSS", f"${fmt_price(plan['stop_loss'])}",
1474
+ f"{plan['stop_type']} · %{plan['risk_frac'] * 100:.2f} risk", "down"),
1475
+ ]
1476
+ for k, (px, m) in enumerate(plan["targets"]):
1477
+ plan_cards.append(metric_card(f"HEDEF {k + 1}", f"${fmt_price(px)}", f"{m:.2f}R", "up"))
1478
+ sz = plan["sizing"]
1479
+ if sz:
1480
+ size_cards = [
1481
+ metric_card("RİSK TUTARI", f"${fmt_large(sz['risk_amount'])}", f"Hesabın %{risk_pct:g}'i"),
1482
+ metric_card("POZİSYON (USDT)", f"${fmt_large(sz['notional'])}", f"Miktar: {sz['qty']:.6g}"),
1483
+ metric_card("KALDIRAÇ ETKİSİ", f"{sz['leverage']:.2f}x",
1484
+ "Spot ile yetmez — kaldıraç gerekir" if sz["leverage"] > 1.0 else "Spot ile uygulanabilir",
1485
+ "down" if sz["leverage"] > 1.0 else "up"),
1486
+ ]
1487
+ stats = bt["stats"] if bt else None
1488
+ if stats is not None:
1489
+ k = stats["kelly"]
1490
+ if stats["n"] < 20:
1491
+ kv, ks, kt = "—", "Örnek az (<20 işlem)", "neutral"
1492
+ elif k is None or k <= 0:
1493
+ kv, ks, kt = "Kenar yok", "Kelly ≤ 0", "down"
1494
+ else:
1495
+ half = min(k / 2, 0.03)
1496
+ kv, ks, kt = f"%{half * 100:.2f}", f"Yarım-Kelly (tavan %3) · bilgi amaçlı", "up"
1497
+ size_cards.append(metric_card("ÖNERİLEN RİSK", kv, ks, kt))
1498
+
1499
+ # ---- Backtest kartları ----
1500
+ bt_cards = []
1501
+ if stats:
1502
+ L, S = stats["long"], stats["short"]
1503
+ wr = lambda x: f"%{x['win_rate'] * 100:.0f}" if x["win_rate"] is not None else "—"
1504
+ bt_cards = [
1505
+ metric_card("İŞLEM SAYISI", f"{stats['n']}", f"Long {L['n']} ({wr(L)}) · Short {S['n']} ({wr(S)})"),
1506
+ metric_card("KAZANMA ORANI", f"%{stats['win_rate'] * 100:.1f}", f"Maks. üst üste kayıp: {stats['max_consec_loss']}",
1507
+ "up" if stats["win_rate"] >= 0.45 else "down"),
1508
+ metric_card("PROFIT FACTOR", f"{stats['profit_factor']:.2f}", "Brüt kazanç / brüt kayıp",
1509
+ "up" if stats["profit_factor"] >= 1.2 else "down"),
1510
+ metric_card("BEKLENTİ / İŞLEM", fmt_pct(stats["expectancy"]), f"Ort. {stats['avg_r']:+.2f}R (komisyon dahil)",
1511
+ "up" if stats["expectancy"] > 0 else "down"),
1512
+ metric_card("SQN", f"{stats['sqn']:.2f}", "Sistem kalitesi (>2 iyi, >3 çok iyi)",
1513
+ "up" if stats["sqn"] >= 2 else "neutral"),
1514
+ metric_card("STRATEJİ MAKS. DD", fmt_pct(stats["max_dd"]), "", "down"),
1515
+ metric_card("STRATEJİ vs AL&TUT", f"{fmt_pct(stats['total_return'])} / {fmt_pct(stats['bh_return'])}",
1516
+ "Aynı dönem, komisyon dahil", "up" if stats["total_return"] > stats["bh_return"] else "down"),
1517
+ ]
1518
+ acc_cards = []
1519
+ if bt and bt["acc"]:
1520
+ a = bt["acc"]
1521
+ acc_cards.append(metric_card("YÖN DOĞRULUĞU", f"%{a['accuracy'] * 100:.1f}",
1522
+ f"{a['n_signals']} sinyal · {a['forward_bars']} bar ileri",
1523
+ "up" if a["accuracy"] >= 0.5 else "down"))
1524
+ if a.get("trend_regime"):
1525
+ t = a["trend_regime"]
1526
+ acc_cards.append(metric_card("DOĞRULUK · TREND", f"%{t['accuracy'] * 100:.1f}",
1527
+ f"{t['n_signals']} sinyal (ADX ≥ 25)", "up" if t["accuracy"] >= 0.5 else "down"))
1528
+ if a.get("range_regime"):
1529
+ t = a["range_regime"]
1530
+ acc_cards.append(metric_card("DOĞRULUK · YATAY", f"%{t['accuracy'] * 100:.1f}",
1531
+ f"{t['n_signals']} sinyal (ADX &lt; 25)", "up" if t["accuracy"] >= 0.5 else "down"))
1532
+
1533
+ # ---- Piyasa bağlamı ----
1534
+ ctx = []
1535
+ for m in res["mtf"]:
1536
+ if m["aligned"] is None:
1537
+ ctx.append(metric_card(f"{m['bar']} TEYİDİ", "Nötr", f"Üst zaman dilimi skoru: {m['score']}"))
1538
+ else:
1539
+ ctx.append(metric_card(f"{m['bar']} TEYİDİ", "Uyumlu ✓" if m["aligned"] else "Ters Yönlü ⚠",
1540
+ f"Üst zaman dilimi skoru: {m['score']}", "up" if m["aligned"] else "down"))
1541
+ if res["funding"] is not None:
1542
+ f_ = res["funding"]
1543
+ ctx.append(metric_card("FUNDING (8S)", f"{f_ * 100:+.4f}%", "Pozitif = long kalabalık",
1544
+ "down" if f_ > 0.0002 else ("up" if f_ < -0.0002 else "neutral")))
1545
+ if res["imbalance"] is not None:
1546
+ im = res["imbalance"]
1547
+ ctx.append(metric_card("DEFTER DENGESİ", f"{im * 100:+.0f}%", "Derinlik ağırlıklı",
1548
+ "up" if im > 0.2 else ("down" if im < -0.2 else "neutral")))
1549
+ if res["supports"]:
1550
+ lv, tc = res["supports"][0]
1551
+ ctx.append(metric_card("YAKIN DESTEK", f"${fmt_price(lv)}", f"{tc} temas · {(res['price'] - lv) / res['atr']:.1f} ATR", "up"))
1552
+ if res["resists"]:
1553
+ lv, tc = res["resists"][0]
1554
+ ctx.append(metric_card("YAKIN DİRENÇ", f"${fmt_price(lv)}", f"{tc} temas · {(lv - res['price']) / res['atr']:.1f} ATR", "down"))
1555
+ ctx.append(metric_card("VOLATİLİTE YÜZDELİĞİ", f"%{res['atr_rank'] * 100:.0f}", "ATR/fiyat, son 100 bar",
1556
+ "down" if res["atr_rank"] > 0.9 else "neutral"))
1557
+
1558
+ reasons_box = (f'<div class="reasons-box"><div class="mc-label">FAKTÖR ANALİZİ</div>'
1559
+ f'<ul>{_reasons_html(res["reasons"])}</ul></div>')
1560
+ bt_note = ""
1561
+ if stats:
1562
+ bt_note = ('<div class="note">Backtest: son ~220 barda sinyaller üst üste binmeden işleme çevrilir; giriş bir sonraki barın açılışı, '
1563
+ f'çıkış SL {SL_ATR}×ATR / TP {TP_ATR}×ATR / {MAX_HOLD_BARS} bar; komisyon %{FEE_PER_SIDE * 100:.1f}/yön dahildir. '
1564
+ 'Emir defteri, funding, S/R ve MTF katmanları canlıdır, geçmişe uygulanamaz.</div>')
1565
+
1566
+ html = (panel + consensus + conf_html + tag_html
1567
+ + (section("İŞLEM PLANI") + cards_row(plan_cards) if plan_cards else "")
1568
+ + (section("POZİSYON BOYUTU") + cards_row(size_cards) if size_cards else "")
1569
+ + (section("GEÇMİŞ PERFORMANS (WALK-FORWARD)") + cards_row(bt_cards) + cards_row(acc_cards) + bt_note
1570
+ if (bt_cards or acc_cards) else "")
1571
+ + section("PİYASA BAĞLAMI") + cards_row(ctx) + reasons_box)
1572
+ return (html, build_signal_price_chart(df, ind, symbol, bar, res), build_factor_chart(res["contribs"], symbol),
1573
+ build_backtest_chart(bt, df, symbol), build_signal_chart(df, ind, symbol))
1574
+ except Exception as e:
1575
+ return empty5(error_box(f"Veri alınamadı: {e}"))
1576
+
1577
+
1578
+ def load_risk(symbol, bar):
1579
+ try:
1580
+ df = fetch_candles(symbol, bar, 300)
1581
+ rm = risk_metrics(df, bar)
1582
+ if rm is None:
1583
+ return error_box("Risk hesaplaması için yeterli veri yok."), go.Figure()
1584
+ cards = [
1585
+ metric_card("YILLIKLANDIRILMIŞ VOLATİLİTE", fmt_pct(rm["volatility_annual"])),
1586
+ metric_card("YILLIKLANDIRILMIŞ GETİRİ", fmt_pct(rm["return_annual"]), "",
1587
+ "up" if rm["return_annual"] >= 0 else "down"),
1588
+ metric_card("SHARPE ORANI", f"{rm['sharpe']:.2f}", "", "up" if rm["sharpe"] > 0 else "down"),
1589
+ metric_card("MAKS. DRAWDOWN", fmt_pct(rm["max_drawdown"]), "", "down"),
1590
+ metric_card("GÜNCEL DRAWDOWN", fmt_pct(rm["current_dd"]), "",
1591
+ "down" if rm["current_dd"] < 0 else "neutral"),
1592
+ metric_card("VaR %95 (BAR)", fmt_pct(rm["var95_bar"]), "", "down"),
1593
+ metric_card("CVaR %95 (BAR)", fmt_pct(rm["cvar95_bar"]), "", "down"),
1594
+ ]
1595
+ return cards_row(cards), build_equity_chart(df, symbol)
1596
+ except Exception as e:
1597
+ return error_box(f"Veri alınamadı: {e}"), go.Figure()
1598
+
1599
+
1600
+ def load_orderbook(symbol, depth):
1601
+ try:
1602
+ ob = fetch_orderbook(symbol, int(depth))
1603
+ bids = [(float(p), float(s)) for p, s, *_ in ob.get("bids", [])]
1604
+ asks = [(float(p), float(s)) for p, s, *_ in ob.get("asks", [])]
1605
+ if not bids or not asks:
1606
+ return error_box("Emir defteri verisi alınamadı."), go.Figure()
1607
+ best_bid, best_ask = bids[0][0], asks[0][0]
1608
+ spread = best_ask - best_bid
1609
+ spread_pct = spread / best_ask if best_ask else 0.0
1610
+ bid_vol = sum(s for _, s in bids)
1611
+ ask_vol = sum(s for _, s in asks)
1612
+ imbalance = (bid_vol - ask_vol) / (bid_vol + ask_vol) if (bid_vol + ask_vol) else 0.0
1613
+ tone = "up" if imbalance > 0.05 else ("down" if imbalance < -0.05 else "neutral")
1614
+ cards = [
1615
+ metric_card("EN İYİ ALIŞ", f"${fmt_price(best_bid)}", "", "up"),
1616
+ metric_card("EN İYİ SATIŞ", f"${fmt_price(best_ask)}", "", "down"),
1617
+ metric_card("SPREAD", f"${fmt_price(spread)}", fmt_pct(spread_pct)),
1618
+ metric_card("DEFTER DENGESİZLİĞİ", fmt_pct(imbalance), "Pozitif = alış baskısı", tone),
1619
+ ]
1620
+ return cards_row(cards), build_depth_chart(bids, asks, symbol)
1621
+ except Exception as e:
1622
+ return error_box(f"Veri alınamadı: {e}"), go.Figure()
1623
+
1624
+
1625
+ @_gpu
1626
+ def refresh_all(symbol, bar, depth, closed_only, acct, risk_pct):
1627
+ ov_html, ov_fig = load_overview(symbol, bar)
1628
+ sig = load_signal(symbol, bar, closed_only, acct, risk_pct)
1629
+ risk_html, risk_fig = load_risk(symbol, bar)
1630
+ ob_html, ob_fig = load_orderbook(symbol, depth)
1631
+ return (ov_html, ov_fig, *sig, risk_html, risk_fig, ob_html, ob_fig)
1632
+
1633
+
1634
+ def refresh_signal(symbol, bar, closed_only, acct, risk_pct):
1635
+ """Yalnızca sinyal sekmesini yeniler (risk/kapanmış-mum ayarı değişince hızlı güncelleme)."""
1636
+ return load_signal(symbol, bar, closed_only, acct, risk_pct)
1637
+
1638
+
1639
+ SCAN_MODES = ["Tümü (skora göre)", "Yalnızca AL yönlü", "Yalnızca SAT yönlü", "Yalnızca yüksek kalite (A / A+)"]
1640
+
1641
+
1642
+ def _scan_one(sym, bar, all_tickers, closed_only):
1643
+ df = fetch_candles(sym, bar, 300)
1644
+ if closed_only:
1645
+ df = df[df["confirm"] == 1].reset_index(drop=True)
1646
+ # MTF ve mikro-yapı katmanları taramada kapalı (API yükünü azaltmak için); tekil analizde açık.
1647
+ res = generate_signal(df, symbol=sym, bar=bar, mtf=False, micro=False, closed_only=closed_only)
1648
+ rm = risk_metrics(df, bar)
1649
+ t = all_tickers.get(sym, {})
1650
+ last = float(t.get("last") or df["c"].iloc[-1])
1651
+ open24h = float(t.get("open24h") or df["c"].iloc[0])
1652
+ chg24h = (last - open24h) / open24h if open24h else 0.0
1653
+ div = res["divergence"]
1654
+ div_txt = "Yükseliş" if div["bullish"] else ("Düşüş" if div["bearish"] else "—")
1655
+ bt = res["backtest"]
1656
+ st = bt["stats"] if bt else None
1657
+ acc = bt["acc"] if bt else None
1658
+ return {
1659
+ "Sembol": sym, "Fiyat": last, "24S %": round(chg24h * 100, 2), "Sinyal": res["signal"],
1660
+ "Skor": res["score"], "Kalite": res["grade"], "Güven %": res["confidence"], "Rejim": res["regime"],
1661
+ "RSI": round(res["indicators"]["rsi"].iloc[-1], 1), "ADX": round(res["adx"], 1),
1662
+ "Sıkışma": "●" if res["squeeze_on"] else "—", "Uyumsuzluk": div_txt,
1663
+ "Kazanma %": round(st["win_rate"] * 100, 1) if st else None,
1664
+ "Profit Factor": round(st["profit_factor"], 2) if st else None,
1665
+ "Beklenti %": round(st["expectancy"] * 100, 2) if st else None,
1666
+ "Yön Doğruluğu %": round(acc["accuracy"] * 100, 1) if acc else None,
1667
+ "Volatilite (Yıllık) %": round(rm["volatility_annual"] * 100, 1) if rm else None,
1668
+ "Sharpe": round(rm["sharpe"], 2) if rm else None,
1669
+ }
1670
+
1671
+
1672
+ def run_scanner(symbols_text, bar, closed_only=False, mode=SCAN_MODES[0], progress=gr.Progress()):
1673
+ symbols = [s.strip().upper() for s in symbols_text.split(",") if s.strip()][:25]
1674
+ if not symbols:
1675
+ return pd.DataFrame()
1676
+ try:
1677
+ all_tickers = {t["instId"]: t for t in fetch_all_tickers("SPOT")}
1678
+ except Exception:
1679
+ all_tickers = {}
1680
+
1681
+ rows, done = [], 0
1682
+ with ThreadPoolExecutor(max_workers=4) as ex: # ağ beklemesini paralelle (OKX limiti için 4 ile sınırlı)
1683
+ futs = {ex.submit(_scan_one, s, bar, all_tickers, closed_only): s for s in symbols}
1684
+ for f in as_completed(futs):
1685
+ done += 1
1686
+ progress(done / len(symbols), desc=f"{futs[f]} tarandı ({done}/{len(symbols)})")
1687
+ try:
1688
+ rows.append(f.result())
1689
+ except Exception:
1690
+ continue
1691
+ if not rows:
1692
+ return pd.DataFrame()
1693
+ out = pd.DataFrame(rows)
1694
+ if mode == SCAN_MODES[1]:
1695
+ out = out[out["Skor"] >= SIGNAL_THRESHOLD].sort_values("Skor", ascending=False)
1696
+ elif mode == SCAN_MODES[2]:
1697
+ out = out[out["Skor"] <= -SIGNAL_THRESHOLD].sort_values("Skor", ascending=True)
1698
+ elif mode == SCAN_MODES[3]:
1699
+ out = out[out["Kalite"].isin(["A", "A+"])].sort_values("Güven %", ascending=False)
1700
+ else:
1701
+ out = out.sort_values("Skor", ascending=False)
1702
+ return out.reset_index(drop=True)
1703
+
1704
+
1705
+ # ---------------------------------------------------------------------------
1706
+ # STYLE — dark "trading terminal" identity (deliberate, not default SaaS chrome)
1707
+ # ---------------------------------------------------------------------------
1708
+ HEAD = """
1709
+ <link rel="preconnect" href="https://fonts.googleapis.com">
1710
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1711
+ <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
1712
+ """
1713
+
1714
+ CSS = """
1715
+ :root {
1716
+ --bg-void: #0a0e14;
1717
+ --bg-panel: #12171f;
1718
+ --line: #232a35;
1719
+ --text-primary: #e6ebf2;
1720
+ --text-muted: #7c8797;
1721
+ --accent-long: #3ddc97;
1722
+ --accent-short: #ff5c72;
1723
+ --accent-signal: #f2b84b;
1724
+ --accent-primary: #5b8cff;
1725
+ }
1726
+ .gradio-container { background: var(--bg-void) !important; color: var(--text-primary) !important;
1727
+ font-family: 'IBM Plex Sans', system-ui, sans-serif !important; }
1728
+ h1, h2, h3 { font-family: 'Space Grotesk', sans-serif !important; letter-spacing: -0.01em; }
1729
+
1730
+ #masthead { display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:10px;
1731
+ border-bottom: 1px solid var(--line); padding: 14px 4px 18px 4px; margin-bottom: 6px; }
1732
+ #masthead .wordmark { font-family:'Space Grotesk',sans-serif; font-weight:700; font-size:1.5rem; letter-spacing:-0.02em; }
1733
+ #masthead .wordmark span { color: var(--accent-primary); }
1734
+ #masthead .tags { display:flex; gap:8px; flex-wrap:wrap; }
1735
+ #masthead .tag { font-size:0.7rem; padding:3px 9px; border:1px solid var(--line); border-radius:3px;
1736
+ color:var(--text-muted); font-family:'IBM Plex Mono',monospace; }
1737
+
1738
+ .metric-row { display:flex; flex-wrap:wrap; gap:10px; margin:8px 0 16px 0; }
1739
+ .metric-card { background: var(--bg-panel); border:1px solid var(--line); border-radius:4px;
1740
+ padding:12px 16px; min-width:150px; flex:1; }
1741
+ .mc-label { font-size:0.66rem; color:var(--text-muted); letter-spacing:0.04em; margin-bottom:6px; }
1742
+ .mc-value { font-size:1.24rem; font-weight:600; color:var(--text-primary);
1743
+ font-family:'IBM Plex Mono',monospace; }
1744
+ .mc-sub { font-size:0.72rem; color:var(--text-muted); margin-top:4px; font-family:'IBM Plex Mono',monospace; }
1745
+ .metric-card.mc-up .mc-value { color: var(--accent-long); }
1746
+ .metric-card.mc-down .mc-value { color: var(--accent-short); }
1747
+
1748
+ .section-title { font-family:'Space Grotesk',sans-serif; font-size:0.78rem; font-weight:600; letter-spacing:0.08em;
1749
+ color:var(--text-muted); margin:18px 0 0 0; padding-top:12px; border-top:1px solid var(--line); }
1750
+
1751
+ .signal-panel { display:flex; align-items:center; flex-wrap:wrap; gap:20px; background:var(--bg-panel);
1752
+ border:1px solid var(--line); border-radius:4px; padding:18px 20px; margin-bottom:12px; }
1753
+ .signal-badge { font-family:'Space Grotesk',sans-serif; font-weight:700; font-size:1.6rem; color:var(--tone);
1754
+ border:2px solid var(--tone); border-radius:4px; padding:10px 22px; }
1755
+ .signal-meta .mc-value { font-size:1.35rem; }
1756
+
1757
+ .consensus { background:var(--bg-panel); border:1px solid var(--line); border-radius:4px; padding:12px 16px; margin-bottom:12px; }
1758
+ .cbar { display:flex; height:8px; border-radius:2px; overflow:hidden; background:var(--line); }
1759
+ .cbull { background: var(--accent-long); }
1760
+ .cbear { background: var(--accent-short); }
1761
+
1762
+ .tags { display:flex; gap:8px; flex-wrap:wrap; }
1763
+ .tags .tag { font-size:0.72rem; font-weight:600; padding:5px 12px; border:1px solid var(--line);
1764
+ border-radius:3px; font-family:'IBM Plex Mono',monospace; letter-spacing:0.03em; }
1765
+
1766
+ .reasons-box { background:var(--bg-panel); border:1px solid var(--line); border-radius:4px; padding:16px 20px; margin-top:8px; }
1767
+ .reasons-box ul { margin:8px 0 0 0; padding-left:0; list-style:none; }
1768
+ .reasons-box li { margin-bottom:6px; color:var(--text-primary); font-size:0.9rem; }
1769
+ .reasons-box .ri { display:inline-block; width:18px; font-size:0.7rem; }
1770
+ .reasons-box .r-up .ri { color: var(--accent-long); }
1771
+ .reasons-box .r-down .ri { color: var(--accent-short); }
1772
+ .reasons-box .r-flat .ri { color: var(--text-muted); }
1773
+
1774
+ .note { color:var(--text-muted); font-size:0.74rem; line-height:1.5; margin:-6px 0 8px 0; }
1775
+
1776
+ .error-box { background: rgba(255,92,114,0.08); border:1px solid var(--accent-short); color:var(--accent-short);
1777
+ padding:14px 18px; border-radius:4px; font-family:'IBM Plex Mono',monospace; font-size:0.85rem; }
1778
+
1779
+ .disclaimer { border-top:1px solid var(--line); margin-top:22px; padding-top:14px; color:var(--text-muted);
1780
+ font-size:0.76rem; line-height:1.55; }
1781
+ """
1782
+
1783
+ # ---------------------------------------------------------------------------
1784
+ # GRADIO UI
1785
+ # ---------------------------------------------------------------------------
1786
+ with gr.Blocks(title="OKX Quant Terminal") as demo:
1787
+
1788
+ gr.HTML(
1789
+ '<div id="masthead">'
1790
+ '<div class="wordmark">OKX <span>QUANT</span> TERMINAL</div>'
1791
+ '<div class="tags"><div class="tag">ELITE SIGNAL ENGINE v2</div>'
1792
+ '<div class="tag">PUBLIC MARKET DATA</div>'
1793
+ '<div class="tag">READ-ONLY · EMİR GÖNDERİLMEZ</div></div>'
1794
+ '</div>'
1795
+ )
1796
+
1797
+ with gr.Row():
1798
+ symbol_dd = gr.Dropdown(choices=POPULAR_SYMBOLS, value="BTC-USDT", allow_custom_value=True,
1799
+ label="Sembol", scale=2)
1800
+ bar_dd = gr.Dropdown(choices=BAR_OPTIONS, value="1H", label="Zaman Dilimi", scale=1)
1801
+ depth_slider = gr.Slider(5, 50, value=20, step=5, label="Defter Derinliği", scale=1)
1802
+ refresh_btn = gr.Button("🔄 Yenile", variant="primary", scale=1)
1803
+ auto_cb = gr.Checkbox(label="Otomatik (15sn)", value=False, scale=1)
1804
+
1805
+ with gr.Row():
1806
+ closed_cb = gr.Checkbox(label="Sadece kapanmış mumlarla sinyal (repaint yok)", value=False, scale=2)
1807
+ acct_num = gr.Number(value=10000, label="Hesap Büyüklüğü (USDT)", minimum=0, scale=1)
1808
+ risk_sl = gr.Slider(0.25, 5, value=1, step=0.25, label="İşlem Başına Risk (%)", scale=1)
1809
+
1810
+ timer = gr.Timer(15, active=False)
1811
+
1812
+ with gr.Tabs():
1813
+ with gr.Tab("📊 Genel Bakış"):
1814
+ ov_html = gr.HTML()
1815
+ ov_plot = gr.Plot()
1816
+ with gr.Tab("🎯 Sinyal Motoru"):
1817
+ sig_html = gr.HTML()
1818
+ sig_map_plot = gr.Plot()
1819
+ sig_factor_plot = gr.Plot()
1820
+ sig_bt_plot = gr.Plot()
1821
+ sig_ind_plot = gr.Plot()
1822
+ with gr.Tab("⚠️ Risk Analitiği"):
1823
+ risk_html = gr.HTML()
1824
+ risk_plot = gr.Plot()
1825
+ with gr.Tab("📖 Emir Defteri"):
1826
+ ob_html = gr.HTML()
1827
+ ob_plot = gr.Plot()
1828
+ with gr.Tab("🔍 Çoklu Varlık Tarayıcı"):
1829
+ gr.Markdown("Popüler pariteleri kompozit sinyal skoruna göre sıralar. Kendi listenizi virgülle ayırıp girebilirsiniz (en fazla 25 sembol).")
1830
+ with gr.Row():
1831
+ scan_symbols = gr.Textbox(value=",".join(POPULAR_SYMBOLS), label="Semboller", scale=3)
1832
+ scan_bar = gr.Dropdown(choices=BAR_OPTIONS, value="4H", label="Zaman Dilimi", scale=1)
1833
+ scan_mode = gr.Dropdown(choices=SCAN_MODES, value=SCAN_MODES[0], label="Filtre", scale=2)
1834
+ scan_btn = gr.Button("🔍 Taramayı Başlat", variant="primary")
1835
+ scan_out = gr.Dataframe(interactive=False, wrap=True)
1836
+
1837
+ gr.HTML(
1838
+ '<div class="disclaimer">Bu panel yalnızca eğitim ve bilgilendirme amaçlıdır; yatırım tavsiyesi '
1839
+ 'değildir. Tüm veriler OKX genel (public) piyasa API\'sinden alınır; hiçbir API anahtarı '
1840
+ 'kullanılmaz ve hiçbir gerçek emir gönderilmez. Kripto varlıklar yüksek oynaklık içerir; geçmiş '
1841
+ 'performans (backtest dahil) gelecekteki sonuçların göstergesi değildir. Backtest sonuçları '
1842
+ 'kısa örneklem üzerindedir ve istatistiksel olarak sınırlıdır.</div>'
1843
+ )
1844
+
1845
+ io_inputs = [symbol_dd, bar_dd, depth_slider, closed_cb, acct_num, risk_sl]
1846
+ io_outputs = [ov_html, ov_plot, sig_html, sig_map_plot, sig_factor_plot, sig_bt_plot, sig_ind_plot,
1847
+ risk_html, risk_plot, ob_html, ob_plot]
1848
+ sig_inputs = [symbol_dd, bar_dd, closed_cb, acct_num, risk_sl]
1849
+ sig_outputs = [sig_html, sig_map_plot, sig_factor_plot, sig_bt_plot, sig_ind_plot]
1850
+
1851
+ refresh_btn.click(refresh_all, io_inputs, io_outputs)
1852
+ timer.tick(refresh_all, io_inputs, io_outputs)
1853
+ auto_cb.change(lambda active: gr.Timer(active=active), auto_cb, timer)
1854
+ closed_cb.change(refresh_signal, sig_inputs, sig_outputs)
1855
+ acct_num.submit(refresh_signal, sig_inputs, sig_outputs)
1856
+ risk_sl.release(refresh_signal, sig_inputs, sig_outputs)
1857
+ scan_btn.click(run_scanner, [scan_symbols, scan_bar, closed_cb, scan_mode], scan_out)
1858
+ demo.load(refresh_all, io_inputs, io_outputs)
1859
+
1860
+ demo.queue().launch(
1861
+ theme=gr.themes.Base(primary_hue="blue", neutral_hue="slate"),
1862
+ css=CSS,
1863
+ head=HEAD,
1864
+ ssr_mode=False,
1865
+ )
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ spaces
2
+ gradio
3
+ pandas
4
+ numpy
5
+ plotly
6
+ requests