diff --git "a/app.py" "b/app.py" new file mode 100644--- /dev/null +++ "b/app.py" @@ -0,0 +1,2104 @@ +""" +OKX QUANT TERMINAL · v2 — ELITE SIGNAL ENGINE +Hedge-fund-style crypto market analytics dashboard built on Gradio. + +Uses ONLY OKX's public (unauthenticated) market-data REST API: + - /api/v5/market/candles + - /api/v5/market/ticker + - /api/v5/market/tickers + - /api/v5/market/books + - /api/v5/public/funding-rate (türev duyarlılığı, isteğe bağlı) + +No API key, no order placement, no account access. This is a read-only +analytics / signal-generation terminal, not an execution bot. + +v2 SİNYAL MOTORU — YENİLİKLER + * Yeni faktörler: Supertrend, Ichimoku bulutu, Donchian kırılımı (hacim teyitli), + piyasa yapısı (HH/HL – LH/LL), CMF, MFI, mum formasyonları, Bollinger/Keltner sıkışması + * Rejim ağırlıkları artık ADX ile SÜREKLİ (yumuşak) geçiş yapar (eski: sert eşik) + * Skor tanh ile ±10'a yumuşak sıkıştırılır; "GÜÇLÜ" sinyal için faktör konsensüsü + MTF şartı + * 2 kademeli çoklu zaman dilimi (MTF) teyidi + * Canlı mikro-yapı katmanı: emir defteri dengesizliği, funding rate, yakın destek/direnç + * Yapısal (S/R) stop + ATR stop, 3 kademeli hedef, hesap büyüklüğüne göre pozisyon boyutu + * Gerçek işlem simülasyonu (SL/TP/zaman çıkışı + komisyon): kazanma oranı, profit factor, + beklenti, R çarpanı, SQN, drawdown, Al&Tut karşılaştırması, yarım-Kelly risk önerisi + * Bileşenli GÜVEN skoru + A+ … D kalite notu, faktör katkı grafiği, sinyal işaretli fiyat grafiği + * "Sadece kapanmış mumlar" modu (repaint yok), veri önbelleği, paralel veri çekimi + * Gann HiLo Activator (trend yönü + dönüş) ve Fibonacci düzeltme seviyeleri (0.236–0.786, + altın bölge 0.382–0.618) artık kompozit skoru ve dolayısıyla AL/SAT sinyalini değiştirir + * Hacim ısı haritası (fiyat × hacim yoğunluğu: POC, değer alanı, sıcak/soğuk bölge) ve canlı + emir defteri likidite duvarları da skoru değiştirir; grafikte ısı haritası arka planda çizilir +""" + +import math +import threading +import time +from concurrent.futures import ThreadPoolExecutor, as_completed + +import numpy as np +import pandas as pd +import plotly.graph_objects as go +from plotly.subplots import make_subplots +import requests +import gradio as gr + +try: # Hugging Face Spaces (ZeroGPU) — yerelde yoksa sessizce atla + import spaces + _gpu = spaces.GPU(duration=30) +except Exception: + def _gpu(fn): + return fn + +# --------------------------------------------------------------------------- +# CONFIG +# --------------------------------------------------------------------------- +OKX_BASE = "https://www.okx.com" +TIMEOUT = 10 +CACHE_TTL = 8 # saniye — aynı mum verisini sekmeler arasında tekrar çekmemek için + +POPULAR_SYMBOLS = [ + "BTC-USDT", "ETH-USDT", "SOL-USDT", "BNB-USDT", "XRP-USDT", + "DOGE-USDT", "ADA-USDT", "AVAX-USDT", "LINK-USDT", "TON-USDT", + "TRX-USDT", "DOT-USDT", "MATIC-USDT", "LTC-USDT", "NEAR-USDT", + "SUI-USDT", "APT-USDT", "ARB-USDT", "OP-USDT", "INJ-USDT", +] + +BAR_OPTIONS = ["1m", "3m", "5m", "15m", "30m", "1H", "2H", "4H", "6H", "12H", "1D", "1W"] + +# approximate periods-per-year for each bar size, used to annualize risk stats +BAR_TO_PERIODS_PER_YEAR = { + "1m": 365 * 24 * 60, "3m": 365 * 24 * 20, "5m": 365 * 24 * 12, + "15m": 365 * 24 * 4, "30m": 365 * 24 * 2, "1H": 365 * 24, + "2H": 365 * 12, "4H": 365 * 6, "6H": 365 * 4, "12H": 365 * 2, + "1D": 365, "1W": 52, +} + +CHART_BG = "#0a0e14" + +# her zaman dilimi için "bir üst" zaman dilimi -> çoklu zaman dilimi (MTF) teyidi için +MTF_MAP = { + "1m": "15m", "3m": "30m", "5m": "1H", "15m": "4H", "30m": "4H", + "1H": "4H", "2H": "1D", "4H": "1D", "6H": "1D", "12H": "1W", + "1D": "1W", "1W": "1W", +} +SLOW_BARS = {"4H", "6H", "12H", "1D", "1W"} # bu vadelerde emir defteri etkisi yarıya iner + +# --- Sinyal motoru parametreleri --- +SIGNAL_THRESHOLD = 2.0 # |skor| >= bu değer -> AL / SAT +STRONG_THRESHOLD = 4.5 # |skor| >= bu değer + konsensüs -> GÜÇLÜ +WARMUP_BARS = 80 # Ichimoku (52+26) ısınması için minimum bar +BACKTEST_LOOKBACK = 220 # walk-forward test penceresi (bar) +SL_ATR, TP_ATR = 1.5, 2.5 # backtest ve varsayılan işlem planı (ATR çarpanları) +MAX_HOLD_BARS = 24 # zaman çıkışı +HILO_PERIOD = 10 # Gann HiLo Activator ortalama periyodu +FIB_LOOKBACK = 89 # Fibonacci swing penceresi (bar) — bir Fibonacci sayısı +FIB_MIN_BARS = 34 # pencere dolmadan da hesaplamaya başlamak için minimum bar +FIB_MIN_RANGE_ATR = 2.5 # swing aralığı bu kadar ATR'den küçükse Fib anlamsız sayılır +FIB_RATIOS = (0.236, 0.382, 0.5, 0.618, 0.786) +HEAT_WINDOW = 100 # hacim ısı haritası penceresi (bar) +HEAT_BINS = 40 # fiyat dilimi sayısı +HEAT_COLD = 0.30 # yoğunluk bu orandan düşükse "soğuk bölge" (hacim boşluğu) +DIV_WEIGHT = 4.0 # RSI uyumsuzluğu ham ağırlığı — diğer tüm faktörlerin en az ~1.5 katı (maks. diğerleri ≈ 2.6) +DIV_EXTREME_BONUS = 1.25 # ilk pivot aşırı bölgedeyse (RSI<35 / >65) ağırlık çarpanı +DIV_LOOKBACK = 60 # iki pivotun içinde bulunması gereken pencere (bar) +DIV_WINDOW = 3 # pivot onayı için her iki yanda gereken bar +DIV_MAX_AGE = 20 # son pivot en fazla bu kadar bar önce olmalı (bayat uyumsuzluk sayılmaz) +WALL_MULT = 3.0 # emir defteri duvarı: seviye, taraf medyanının en az bu kadar katı +FEE_PER_SIDE = 0.001 # OKX spot taker ≈ %0.1 / yön +TP_R_MULTIPLES = (TP_ATR / SL_ATR, 2.5, 4.0) # TP1/TP2/TP3 (R cinsinden) + + +# --------------------------------------------------------------------------- +# OKX PUBLIC API WRAPPERS (önbellek + yeniden deneme) +# --------------------------------------------------------------------------- +_CACHE = {} +_CACHE_LOCK = threading.Lock() + + +def _cached(key, ttl, loader): + now = time.time() + with _CACHE_LOCK: + hit = _CACHE.get(key) + if hit and now - hit[0] < ttl: + return hit[1] + val = loader() + with _CACHE_LOCK: + _CACHE[key] = (time.time(), val) + if len(_CACHE) > 200: # eski kayıtları buda + for k in sorted(_CACHE, key=lambda x: _CACHE[x][0])[:60]: + _CACHE.pop(k, None) + return val + + +def _get(path, params=None, retries=2): + for attempt in range(retries + 1): + try: + r = requests.get(f"{OKX_BASE}{path}", params=params, timeout=TIMEOUT) + r.raise_for_status() + data = r.json() + code = str(data.get("code")) + if code == "50011" and attempt < retries: # OKX rate-limit + time.sleep(0.5 * (attempt + 1)) + continue + if code != "0": + raise RuntimeError(data.get("msg") or "OKX API hata döndürdü.") + return data["data"] + except requests.exceptions.RequestException as e: + if attempt < retries: + time.sleep(0.3 * (attempt + 1)) + continue + raise RuntimeError(f"Ağ hatası: {e}") + + +def fetch_candles(inst_id, bar="1H", limit=300): + def _load(): + raw = _get("/api/v5/market/candles", {"instId": inst_id, "bar": bar, "limit": limit}) + if not raw: + raise RuntimeError(f"{inst_id} için mum verisi bulunamadı (sembolü kontrol edin).") + cols = ["ts", "o", "h", "l", "c", "vol", "volCcy", "volCcyQuote", "confirm"] + df = pd.DataFrame(raw, columns=cols) + for c in ["o", "h", "l", "c", "vol"]: + df[c] = df[c].astype(float) + df["confirm"] = df["confirm"].astype(int) + df["ts"] = pd.to_datetime(df["ts"].astype(np.int64), unit="ms") + return df.sort_values("ts").reset_index(drop=True) + + return _cached(("candles", inst_id, bar, limit), CACHE_TTL, _load).copy() + + +def fetch_ticker(inst_id): + raw = _get("/api/v5/market/ticker", {"instId": inst_id}) + return raw[0] if raw else {} + + +def fetch_all_tickers(inst_type="SPOT"): + return _cached(("tickers", inst_type), 20, lambda: _get("/api/v5/market/tickers", {"instType": inst_type})) + + +def fetch_orderbook(inst_id, depth=20): + raw = _get("/api/v5/market/books", {"instId": inst_id, "sz": depth}) + return raw[0] if raw else {} + + +def fetch_funding_rate(inst_id): + """Spot paritenin (BTC-USDT) sürekli vadeli karşılığının (BTC-USDT-SWAP) funding oranı.""" + parts = inst_id.split("-") + if len(parts) != 2: + return None + raw = _get("/api/v5/public/funding-rate", {"instId": f"{inst_id}-SWAP"}) + if not raw: + return None + return float(raw[0].get("fundingRate") or 0.0) + + +# --------------------------------------------------------------------------- +# TECHNICAL INDICATORS +# --------------------------------------------------------------------------- +def ema(s, p): + return s.ewm(span=p, adjust=False).mean() + + +def sma(s, p): + return s.rolling(p).mean() + + +def rsi(s, p=14): + d = s.diff() + gain = d.clip(lower=0) + loss = -d.clip(upper=0) + ag = gain.ewm(alpha=1 / p, adjust=False).mean() + al = loss.ewm(alpha=1 / p, adjust=False).mean() + rs = ag / al.replace(0, np.nan) + return (100 - 100 / (1 + rs)).fillna(50) + + +def macd(s, fast=12, slow=26, signal=9): + line = ema(s, fast) - ema(s, slow) + sig = ema(line, signal) + return line, sig, line - sig + + +def bollinger(s, p=20, k=2): + mid = sma(s, p) + std = s.rolling(p).std() + return mid + k * std, mid, mid - k * std + + +def atr(df, p=14): + h, l, c = df["h"], df["l"], df["c"] + pc = c.shift(1) + tr = pd.concat([h - l, (h - pc).abs(), (l - pc).abs()], axis=1).max(axis=1) + return tr.ewm(alpha=1 / p, adjust=False).mean() + + +def stochastic(df, k_p=14, d_p=3): + lo = df["l"].rolling(k_p).min() + hi = df["h"].rolling(k_p).max() + k = 100 * (df["c"] - lo) / (hi - lo).replace(0, np.nan) + d = k.rolling(d_p).mean() + return k.fillna(50), d.fillna(50) + + +def vwap_window(df): + tp = (df["h"] + df["l"] + df["c"]) / 3 + return (tp * df["vol"]).cumsum() / df["vol"].cumsum().replace(0, np.nan) + + +def adx(df, p=14): + """ADX + yönsel indikatörler (+DI / -DI). Trend gücünü ölçer (0-100).""" + h, l, c = df["h"], df["l"], df["c"] + up_move = h.diff() + down_move = -l.diff() + plus_dm = np.where((up_move > down_move) & (up_move > 0), up_move, 0.0) + minus_dm = np.where((down_move > up_move) & (down_move > 0), down_move, 0.0) + tr = pd.concat([h - l, (h - c.shift(1)).abs(), (l - c.shift(1)).abs()], axis=1).max(axis=1) + atr_ = tr.ewm(alpha=1 / p, adjust=False).mean() + plus_di = 100 * pd.Series(plus_dm, index=df.index).ewm(alpha=1 / p, adjust=False).mean() / atr_.replace(0, np.nan) + minus_di = 100 * pd.Series(minus_dm, index=df.index).ewm(alpha=1 / p, adjust=False).mean() / atr_.replace(0, np.nan) + dx = ((plus_di - minus_di).abs() / (plus_di + minus_di).replace(0, np.nan)) * 100 + adx_ = dx.ewm(alpha=1 / p, adjust=False).mean() + return adx_.fillna(0), plus_di.fillna(0), minus_di.fillna(0) + + +def obv(df): + """On-Balance Volume: hacmin fiyat yönüyle kümülatif toplamı.""" + direction = np.sign(df["c"].diff()).fillna(0) + return (direction * df["vol"]).cumsum() + + +def supertrend(df, period=10, mult=3.0): + """Supertrend: ATR bantlı trend takipçisi. dir=+1 yükseliş, -1 düşüş. (nedensel, geleceğe bakmaz)""" + h, l, c = df["h"].to_numpy(), df["l"].to_numpy(), df["c"].to_numpy() + a = atr(df, period).to_numpy() + hl2 = (h + l) / 2 + ub, lb = hl2 + mult * a, hl2 - mult * a + n = len(c) + fub, flb = ub.copy(), lb.copy() + direction = np.ones(n) + line = np.full(n, np.nan) + if n: + line[0] = flb[0] + for i in range(1, n): + fub[i] = ub[i] if (ub[i] < fub[i - 1] or c[i - 1] > fub[i - 1]) else fub[i - 1] + flb[i] = lb[i] if (lb[i] > flb[i - 1] or c[i - 1] < flb[i - 1]) else flb[i - 1] + if c[i] > fub[i - 1]: + direction[i] = 1 + elif c[i] < flb[i - 1]: + direction[i] = -1 + else: + direction[i] = direction[i - 1] + line[i] = flb[i] if direction[i] == 1 else fub[i] + return pd.Series(line, index=df.index), pd.Series(direction, index=df.index) + + +def ichimoku(df): + """Tenkan/Kijun + bulut. Bulut, 26 bar önce hesaplanan Senkou A/B'nin bugüne kaydırılmış halidir + (yani nedensel: gelecek veri kullanılmaz).""" + h, l = df["h"], df["l"] + tenkan = (h.rolling(9).max() + l.rolling(9).min()) / 2 + kijun = (h.rolling(26).max() + l.rolling(26).min()) / 2 + span_a = (tenkan + kijun) / 2 + span_b = (h.rolling(52).max() + l.rolling(52).min()) / 2 + return tenkan, kijun, span_a.shift(26), span_b.shift(26) + + +def hilo_activator(df, period=HILO_PERIOD): + """Gann HiLo Activator. Kapanış önceki barın SMA(High) değerini aşarsa YÜKSELİŞ (çizgi=SMA(Low)), + önceki barın SMA(Low) değerinin altına inerse DÜŞÜŞ (çizgi=SMA(High)) moduna geçer; arada mod korunur. + Dönüş: (çizgi, yön) yön: +1 / -1 / 0 (henüz belirsiz). Nedensel — geleceğe bakmaz.""" + hi_ma = df["h"].rolling(period).mean() + lo_ma = df["l"].rolling(period).mean() + c = df["c"].to_numpy(dtype=float) + hi_p, lo_p = hi_ma.shift(1).to_numpy(), lo_ma.shift(1).to_numpy() + hi_c, lo_c = hi_ma.to_numpy(), lo_ma.to_numpy() + n = len(c) + direction = np.zeros(n) + line = np.full(n, np.nan) + cur = 0.0 + for i in range(n): + if hi_p[i] == hi_p[i] and c[i] > hi_p[i]: + cur = 1.0 + elif lo_p[i] == lo_p[i] and c[i] < lo_p[i]: + cur = -1.0 + direction[i] = cur + if cur > 0: + line[i] = lo_c[i] + elif cur < 0: + line[i] = hi_c[i] + return pd.Series(line, index=df.index), pd.Series(direction, index=df.index) + + +def fib_swing(df, lookback=FIB_LOOKBACK, min_bars=FIB_MIN_BARS): + """Her bar için son `lookback` bardaki en yüksek/en düşük nokta ve dalga yönü. + yön=+1: dip tepeden ÖNCE (yükseliş dalgası -> düzeltmede destek aranır) + yön=-1: tepe dipten ÖNCE (düşüş dalgası -> tepkide direnç aranır). Nedensel.""" + h, l = df["h"], df["l"] + hi = h.rolling(lookback, min_periods=min_bars).max() + lo = l.rolling(lookback, min_periods=min_bars).min() + hi_pos = h.rolling(lookback, min_periods=min_bars).apply(np.argmax, raw=True) + lo_pos = l.rolling(lookback, min_periods=min_bars).apply(np.argmin, raw=True) + direction = pd.Series(np.where(lo_pos < hi_pos, 1.0, -1.0), index=df.index) + direction = direction.where(hi_pos.notna() & lo_pos.notna()) + return hi, lo, direction + + +def fib_levels(hi, lo, direction): + """Dalga yönüne göre Fibonacci düzeltme fiyatları: [(oran, fiyat), ...]""" + rng = hi - lo + if direction > 0: + return [(r, hi - r * rng) for r in FIB_RATIOS] + return [(r, lo + r * rng) for r in FIB_RATIOS] + + +def _volume_grid(h, l, vol, edges): + """Her barın hacmini, [low, high] aralığının kestiği fiyat dilimlerine eşit dağıtır. -> (bar × dilim)""" + lo_e, hi_e = edges[:-1][None, :], edges[1:][None, :] + overlap = np.clip(np.minimum(h[:, None], hi_e) - np.maximum(l[:, None], lo_e), 0.0, None) + span = np.maximum(h - l, 1e-12)[:, None] + return overlap / span * vol[:, None] + + +def volume_heat(df, window=HEAT_WINDOW, bins=HEAT_BINS): + """Hacim ısı haritası profili (nedensel: her bar yalnızca kendisine kadar olan pencereyi kullanır). + Dönüş: poc, val, vah (%70 değer alanı), heat (kapanışın bulunduğu dilimin yoğunluğu / en yoğun dilim, 0..1)""" + h, l, c, v = (df[k].to_numpy(dtype=float) for k in ("h", "l", "c", "vol")) + n = len(c) + poc, val, vah, heat = (np.full(n, np.nan) for _ in range(4)) + for i in range(min(window, n) - 1, n): + a = max(0, i - window + 1) + hh, ll = h[a:i + 1], l[a:i + 1] + top, bot = hh.max(), ll.min() + if top <= bot: + continue + edges = np.linspace(bot, top, bins + 1) + prof = _volume_grid(hh, ll, v[a:i + 1], edges).sum(axis=0) + if prof.max() <= 0: + continue + mids = (edges[:-1] + edges[1:]) / 2 + k = int(prof.argmax()) + poc[i] = mids[k] + # değer alanı: POC'tan başlayıp toplam hacmin %70'ine ulaşana dek komşu dilimleri ekle + lo_k = hi_k = k + acc, tot = prof[k], prof.sum() + while acc < 0.7 * tot and (lo_k > 0 or hi_k < bins - 1): + up = prof[hi_k + 1] if hi_k < bins - 1 else -1.0 + dn = prof[lo_k - 1] if lo_k > 0 else -1.0 + if up >= dn: + hi_k += 1; acc += prof[hi_k] + else: + lo_k -= 1; acc += prof[lo_k] + val[i], vah[i] = edges[lo_k], edges[hi_k + 1] + ci = int(np.clip(np.searchsorted(edges, c[i], side="right") - 1, 0, bins - 1)) + heat[i] = prof[ci] / prof.max() + return (pd.Series(x, index=df.index) for x in (poc, val, vah, heat)) + + +def book_walls(ob, price, atr_v, mult=WALL_MULT, reach=1.5): + """Emir defterindeki 'sıcak' likidite duvarlarını bulur (yakın ve taraf medyanına göre çok büyük seviyeler). + Dönüş: (destek_duvarı, direnç_duvarı) — her biri (fiyat, büyüklük_kat) ya da None.""" + def pick(levels, below): + if not levels or atr_v <= 0: + return None + sizes = np.array([float(x[1]) for x in levels]) + px = np.array([float(x[0]) for x in levels]) + med = float(np.median(sizes)) + if med <= 0: + return None + dist = (price - px) / atr_v if below else (px - price) / atr_v + ok = (sizes >= mult * med) & (dist > 0) & (dist <= reach) + if not ok.any(): + return None + j = int(np.where(ok, sizes, -1).argmax()) + return float(px[j]), float(sizes[j] / med) + return pick(ob.get("bids"), True), pick(ob.get("asks"), False) + + +def mfi(df, p=14): + """Money Flow Index: hacim ağırlıklı RSI.""" + tp = (df["h"] + df["l"] + df["c"]) / 3 + rmf = tp * df["vol"] + d = tp.diff() + pos = rmf.where(d > 0, 0.0).rolling(p).sum() + neg = rmf.where(d < 0, 0.0).rolling(p).sum() + out = 100 - 100 / (1 + pos / neg.replace(0, np.nan)) + out = out.where(~((neg == 0) & (pos > 0)), 100.0) + return out.fillna(50) + + +def cmf(df, p=20): + """Chaikin Money Flow: kapanışın bar aralığındaki konumuna göre hacim akışı (-1..+1).""" + rng = (df["h"] - df["l"]).replace(0, np.nan) + mfm = ((df["c"] - df["l"]) - (df["h"] - df["c"])) / rng + mfv = mfm.fillna(0) * df["vol"] + return (mfv.rolling(p).sum() / df["vol"].rolling(p).sum().replace(0, np.nan)).fillna(0) + + +def market_structure(df, w=10): + """+1: Higher-High & Higher-Low (yükseliş yapısı), -1: Lower-High & Lower-Low, 0: karışık.""" + hi, lo = df["h"].rolling(w).max(), df["l"].rolling(w).min() + up = (hi > hi.shift(w)) & (lo > lo.shift(w)) + dn = (hi < hi.shift(w)) & (lo < lo.shift(w)) + return pd.Series(np.where(up, 1.0, np.where(dn, -1.0, 0.0)), index=df.index) + + +def squeeze_indicator(df, ind): + """TTM tarzı sıkışma: Bollinger bantları Keltner kanalının İÇİNE girerse volatilite sıkışmıştır. + Dönüş: (sıkışma_açık, son 3 barda sıkışma çözüldü mü, momentum)""" + kc_mid = ind["ema20"] + kc_u, kc_l = kc_mid + 1.5 * ind["atr"], kc_mid - 1.5 * ind["atr"] + on = (ind["bb_lower"] > kc_l) & (ind["bb_upper"] < kc_u) + release = (~on) & on.shift(1, fill_value=False) + rel3 = release.astype(float).rolling(3, min_periods=1).max() + hh, ll = df["h"].rolling(20).max(), df["l"].rolling(20).min() + mom = df["c"] - ((hh + ll) / 2 + sma(df["c"], 20)) / 2 + return on.astype(float), rel3, mom + + +CANDLE_PATTERNS = [ + ("pat_hammer", "Çekiç", 1), + ("pat_bull_eng", "Yükseliş Yutan Mum", 1), + ("pat_morning", "Sabah Yıldızı", 1), + ("pat_shooting", "Kayan Yıldız", -1), + ("pat_bear_eng", "Düşüş Yutan Mum", -1), + ("pat_evening", "Akşam Yıldızı", -1), +] + + +def candle_patterns(df): + """Vektörel mum formasyonu tespiti (yalnızca geçmiş barlar kullanılır).""" + o, h, l, c = df["o"], df["h"], df["l"], df["c"] + body = (c - o).abs() + rng = (h - l).replace(0, np.nan) + upper = h - np.maximum(o, c) + lower = np.minimum(o, c) - l + avg_body = body.rolling(10, min_periods=3).mean() + prior_down = c.shift(1) < c.shift(4) + prior_up = c.shift(1) > c.shift(4) + + hammer = (lower >= 0.6 * rng) & (body <= 0.3 * rng) & (upper <= 0.15 * rng) & prior_down + shooting = (upper >= 0.6 * rng) & (body <= 0.3 * rng) & (lower <= 0.15 * rng) & prior_up + bull_eng = ((c.shift(1) < o.shift(1)) & (c > o) & (c >= o.shift(1)) & (o <= c.shift(1)) + & (body > body.shift(1)) & prior_down) + bear_eng = ((c.shift(1) > o.shift(1)) & (c < o) & (c <= o.shift(1)) & (o >= c.shift(1)) + & (body > body.shift(1)) & prior_up) + big2 = body.shift(2) > avg_body.shift(2) + small1 = body.shift(1) < 0.35 * body.shift(2) + mid2 = (o.shift(2) + c.shift(2)) / 2 + morning = (c.shift(2) < o.shift(2)) & big2 & small1 & (c > o) & (c > mid2) + evening = (c.shift(2) > o.shift(2)) & big2 & small1 & (c < o) & (c < mid2) + + raw = {"pat_hammer": hammer, "pat_shooting": shooting, "pat_bull_eng": bull_eng, + "pat_bear_eng": bear_eng, "pat_morning": morning, "pat_evening": evening} + return {k: v.fillna(False).astype(float) for k, v in raw.items()} + + +def find_swings(series, window=3): + """Yerel tepe/dip (swing high/low) indekslerini bulur.""" + n = len(series) + highs, lows = [], [] + for i in range(window, n - window): + seg = series.iloc[i - window:i + window + 1] + if series.iloc[i] == seg.max(): + highs.append(i) + if series.iloc[i] == seg.min(): + lows.append(i) + return highs, lows + + +def detect_divergence(df, ind, lookback=60, window=3): + """Fiyat ile RSI arasındaki son swing'leri karşılaştırarak klasik uyumsuzluğu tespit eder.""" + n = len(df) + lb = min(lookback, n) + close = df["c"].iloc[-lb:].reset_index(drop=True) + rsi_s = ind["rsi"].iloc[-lb:].reset_index(drop=True) + highs, lows = find_swings(close, window) + result = {"bullish": False, "bearish": False} + if len(highs) >= 2: + h1, h2 = highs[-2], highs[-1] + if close.iloc[h2] > close.iloc[h1] and rsi_s.iloc[h2] < rsi_s.iloc[h1]: + result["bearish"] = True + if len(lows) >= 2: + l1, l2 = lows[-2], lows[-1] + if close.iloc[l2] < close.iloc[l1] and rsi_s.iloc[l2] > rsi_s.iloc[l1]: + result["bullish"] = True + return result + + +def rsi_divergence(close, rsi_s, lookback=DIV_LOOKBACK, window=DIV_WINDOW, max_age=DIV_MAX_AGE): + """Bar-bar NEDENSEL RSI uyumsuzluğu. Bir pivot, ancak `window` bar sonra onaylanır; bar i'de yalnızca + i-window'a kadar onaylı pivotlar kullanılır (geleceğe bakmaz), bu yüzden backtest ile canlı sinyal tutarlıdır. + Dönüş: (bull, bear) — 0 = yok, 1.0 = uyumsuzluk, DIV_EXTREME_BONUS = aşırı bölgeden uyumsuzluk.""" + c = close.to_numpy(dtype=float) + r = rsi_s.to_numpy(dtype=float) + n = len(c) + is_hi, is_lo = np.zeros(n, bool), np.zeros(n, bool) + for j in range(window, n - window): + seg = c[j - window:j + window + 1] + is_hi[j], is_lo[j] = c[j] == seg.max(), c[j] == seg.min() + bull, bear = np.zeros(n), np.zeros(n) + lows, highs = [], [] + for i in range(n): + j = i - window + if j >= window: + if is_lo[j]: + lows.append(j) + if is_hi[j]: + highs.append(j) + start = i - lookback + 1 + if len(lows) >= 2: + l1, l2 = lows[-2], lows[-1] + if l1 >= start and i - l2 <= max_age and c[l2] < c[l1] and r[l2] > r[l1]: + bull[i] = DIV_EXTREME_BONUS if r[l1] < 35 else 1.0 + if len(highs) >= 2: + h1, h2 = highs[-2], highs[-1] + if h1 >= start and i - h2 <= max_age and c[h2] > c[h1] and r[h2] < r[h1]: + bear[i] = DIV_EXTREME_BONUS if r[h1] > 65 else 1.0 + return pd.Series(bull, index=close.index), pd.Series(bear, index=close.index) + + +def find_sr_levels(df, atr_v, window=5, lookback=200, max_levels=4): + """Swing tepe/diplerini kümeleyerek destek/direnç seviyeleri çıkarır. + Dönüş: (destekler, dirençler) — her biri [(seviye, temas_sayısı), ...], fiyata yakınlık sırasıyla.""" + sub = df.iloc[-lookback:].reset_index(drop=True) + highs, _ = find_swings(sub["h"], window) + _, lows = find_swings(sub["l"], window) + pts = [float(sub["h"].iloc[i]) for i in highs] + [float(sub["l"].iloc[i]) for i in lows] + if not pts: + return [], [] + price = float(df["c"].iloc[-1]) + tol = max(0.6 * (atr_v if atr_v and atr_v > 0 else 0.0), price * 0.002) + pts.sort() + clusters, cur = [], [pts[0]] + for p in pts[1:]: + if p - float(np.mean(cur)) <= tol: + cur.append(p) + else: + clusters.append(cur) + cur = [p] + clusters.append(cur) + levels = [(float(np.mean(c)), len(c)) for c in clusters] + supports = sorted([lv for lv in levels if lv[0] < price], key=lambda x: -x[0])[:max_levels] + resists = sorted([lv for lv in levels if lv[0] > price], key=lambda x: x[0])[:max_levels] + return supports, resists + + +def compute_indicators(df): + close = df["c"] + ind = {} + ind["rsi"] = rsi(close) + ind["macd_line"], ind["macd_signal"], ind["macd_hist"] = macd(close) + ind["ema20"] = ema(close, 20) + ind["ema50"] = ema(close, 50) + ind["ema200"] = ema(close, 200 if len(close) >= 200 else max(5, len(close) // 2)) + ind["bb_upper"], ind["bb_mid"], ind["bb_lower"] = bollinger(close) + ind["atr"] = atr(df) + ind["stoch_k"], ind["stoch_d"] = stochastic(df) + ind["vwap"] = vwap_window(df) + ind["adx"], ind["plus_di"], ind["minus_di"] = adx(df) + ind["obv"] = obv(df) + ind["obv_ema"] = ema(ind["obv"], 20) + # --- v2 ek faktörler --- + ind["st_line"], ind["st_dir"] = supertrend(df) + ind["tenkan"], ind["kijun"], ind["ich_a"], ind["ich_b"] = ichimoku(df) + ind["hilo_line"], ind["hilo_dir"] = hilo_activator(df) + ind["fib_hi"], ind["fib_lo"], ind["fib_dir"] = fib_swing(df) + ind["div_bull"], ind["div_bear"] = rsi_divergence(close, ind["rsi"]) + ind["vp_poc"], ind["vp_val"], ind["vp_vah"], ind["vp_heat"] = volume_heat(df) + ind["mfi"] = mfi(df) + ind["cmf"] = cmf(df) + ind["rvol"] = (df["vol"] / sma(df["vol"], 20).replace(0, np.nan)).fillna(1.0) + ind["dc_hi20"] = df["h"].rolling(20).max().shift(1) + ind["dc_lo20"] = df["l"].rolling(20).min().shift(1) + ind["struct"] = market_structure(df) + ind["sq_on"], ind["sq_rel3"], ind["sq_mom"] = squeeze_indicator(df, ind) + ind["atr_rank"] = (ind["atr"] / close).rolling(100, min_periods=30).apply( + lambda x: float((x <= x[-1]).mean()), raw=True).fillna(0.5) + for name, ser in candle_patterns(df).items(): + ind[name] = ser + # hızlı, nedensel erişim için numpy önbelleği (backtest döngüsünde .iloc yerine kullanılır) + arr = {k: v.to_numpy(dtype=float) for k, v in ind.items()} + for k in ("o", "h", "l", "c", "vol"): + arr[k] = df[k].to_numpy(dtype=float) + ind["_arr"] = arr + return ind + + +# --------------------------------------------------------------------------- +# COMPOSITE SIGNAL ENGINE v2 (multi-factor, rejim-uyarlamalı, konsensüs filtreli) +# --------------------------------------------------------------------------- +def _score_at(ind, i, verbose=False): + """Çoklu faktör puanlamasını verilen bar indeksinde hesaplar (yalnızca o ana kadarki veri). + + İki faktör ailesi vardır: + T (TREND) : MACD, EMA dizilimi, VWAP, OBV, Supertrend, HiLo Activator, Ichimoku, + Donchian kırılımı, piyasa yapısı, CMF, sıkışma kırılımı, ADX/DI yönü + D (RSI UYUMSUZLUK): EN YÜKSEK AĞIRLIKLI faktör (DIV_WEIGHT), rejimden bağımsız + F (FİBONACCI) : düzeltme seviyeleri (rejimden bağımsız, ağırlık 1.0) + H (ISI HARİTASI): hacim profili — POC / değer alanı / soğuk bölge (rejimden bağımsız, ağırlık 1.0) + R (REVERSION) : RSI, Bollinger, Stokastik, MFI, mum formasyonları + + ADX'e göre iki ailenin ağırlığı SÜREKLİ olarak kayar (w: 0=yatay … 1=güçlü trend). + Güçlü trendde karşı-trend (aşırı alım/satım) sinyalleri bastırılır; yatay piyasada trend takibi bastırılır. + Sonuç, tanh ile ±10 aralığına yumuşak sıkıştırılır. + Canlı sinyal ve tarihsel backtest AYNI fonksiyonu kullanır (tutarlılık + gelecek sızıntısı yok).""" + A = ind["_arr"] + price = A["c"][i] + items = [] # (etiket, grup, ham_puan, açıklama) + + def add(label, group, raw, text): + items.append((label, group, float(raw), text)) + + # --- RSI (R) --- + rsi_v = A["rsi"][i] + if rsi_v < 30: + add("RSI", "R", 2, f"RSI aşırı satım bölgesinde ({rsi_v:.1f})") + elif rsi_v > 70: + add("RSI", "R", -2, f"RSI aşırı alım bölgesinde ({rsi_v:.1f})") + else: + add("RSI", "R", 0, f"RSI nötr bölgede ({rsi_v:.1f})") + + # --- MACD histogramı (T) --- + hist_v = A["macd_hist"][i] + hist_prev = A["macd_hist"][i - 1] if i > 0 else 0.0 + if hist_v > 0 and hist_v > hist_prev: + add("MACD", "T", 1.5, "MACD histogramı pozitif ve güçleniyor") + elif hist_v > 0: + add("MACD", "T", 0.5, "MACD histogramı pozitif") + elif hist_v < 0 and hist_v < hist_prev: + add("MACD", "T", -1.5, "MACD histogramı negatif ve zayıflıyor") + else: + add("MACD", "T", -0.5, "MACD histogramı negatif") + + # --- EMA dizilimi (T) --- + e20, e50, e200 = A["ema20"][i], A["ema50"][i], A["ema200"][i] + if price > e20 > e50 > e200: + add("EMA Dizilimi", "T", 2, "Güçlü yükseliş trendi (Fiyat > EMA20 > EMA50 > EMA200)") + elif price > e20 and e20 > e50: + add("EMA Dizilimi", "T", 1, "Yükseliş trendi (kısa vadeli ortalamaların üzerinde)") + elif price < e20 < e50 < e200: + add("EMA Dizilimi", "T", -2, "Güçlü düşüş trendi (Fiyat < EMA20 < EMA50 < EMA200)") + elif price < e20 and e20 < e50: + add("EMA Dizilimi", "T", -1, "Düşüş trendi (kısa vadeli ortalamaların altında)") + else: + add("EMA Dizilimi", "T", 0, "Trend yatay / belirsiz") + + # --- Bollinger konumu (R) --- + bb_u, bb_l = A["bb_upper"][i], A["bb_lower"][i] + if bb_u == bb_u and bb_l == bb_l and bb_u != bb_l: + bb_pos = (price - bb_l) / (bb_u - bb_l) + if bb_pos < 0.15: + add("Bollinger", "R", 1, "Fiyat Bollinger alt bandına yakın (olası tepki)") + elif bb_pos > 0.85: + add("Bollinger", "R", -1, "Fiyat Bollinger üst bandına yakın (olası tepki)") + + # --- Stokastik (R) --- + k_v = A["stoch_k"][i] + if k_v < 20: + add("Stokastik", "R", 1, f"Stokastik aşırı satım bölgesinde ({k_v:.1f})") + elif k_v > 80: + add("Stokastik", "R", -1, f"Stokastik aşırı alım bölgesinde ({k_v:.1f})") + + # --- MFI (R) --- + mfi_v = A["mfi"][i] + if mfi_v < 20: + add("MFI", "R", 1, f"MFI (hacimli RSI) aşırı satımda ({mfi_v:.1f})") + elif mfi_v > 80: + add("MFI", "R", -1, f"MFI (hacimli RSI) aşırı alımda ({mfi_v:.1f})") + + # --- Mum formasyonları (R) --- + hits = [] + for key, name, sgn in CANDLE_PATTERNS: + if A[key][i] > 0: + hits.append((name, sgn)) + if hits: + raw = max(-1.5, min(1.5, float(sum(s for _, s in hits)))) + add("Mum Formasyonu", "R", raw, "Mum formasyonu: " + ", ".join(n for n, _ in hits)) + + # --- VWAP (T) --- + vwap_v = A["vwap"][i] + if vwap_v == vwap_v: + if price > vwap_v: + add("VWAP", "T", 0.5, "Fiyat pencere VWAP'ının üzerinde") + else: + add("VWAP", "T", -0.5, "Fiyat pencere VWAP'ının altında") + + # --- OBV (T) --- + obv_v, obv_e = A["obv"][i], A["obv_ema"][i] + if obv_e == obv_e: + if obv_v > obv_e: + add("OBV", "T", 0.5, "OBV hacim akışı yükseliş yönünde") + else: + add("OBV", "T", -0.5, "OBV hacim akışı düşüş yönünde") + + # --- CMF (T) --- + cmf_v = A["cmf"][i] + if cmf_v > 0.05: + add("CMF", "T", 0.5, f"Chaikin para akışı pozitif ({cmf_v:+.2f}) — alıcılar baskın") + elif cmf_v < -0.05: + add("CMF", "T", -0.5, f"Chaikin para akışı negatif ({cmf_v:+.2f}) — satıcılar baskın") + + # --- Supertrend (T) --- + st_d = A["st_dir"][i] + st_flip = i > 0 and A["st_dir"][i] != A["st_dir"][i - 1] + if st_d > 0: + add("Supertrend", "T", 1.5 + (0.5 if st_flip else 0), + "Supertrend YÜKSELİŞ moduna döndü (taze sinyal)" if st_flip else "Supertrend yükseliş modunda") + else: + add("Supertrend", "T", -(1.5 + (0.5 if st_flip else 0)), + "Supertrend DÜŞÜŞ moduna döndü (taze sinyal)" if st_flip else "Supertrend düşüş modunda") + + # --- Gann HiLo Activator (T) --- + hl_d = A["hilo_dir"][i] + hilo_flip = 0 + if hl_d != 0: + prev_d = A["hilo_dir"][i - 1] if i > 0 else 0.0 + if prev_d != 0 and prev_d != hl_d: + hilo_flip = int(hl_d) + mag = 1.25 + (0.5 if hilo_flip else 0.0) + if hl_d > 0: + add("HiLo Activator", "T", mag, + "HiLo Activator YÜKSELİŞ moduna döndü (taze sinyal)" if hilo_flip else "HiLo Activator yükseliş modunda (fiyat HiLo çizgisinin üzerinde)") + else: + add("HiLo Activator", "T", -mag, + "HiLo Activator DÜŞÜŞ moduna döndü (taze sinyal)" if hilo_flip else "HiLo Activator düşüş modunda (fiyat HiLo çizgisinin altında)") + + # --- Fibonacci düzeltme seviyeleri (F) --- + fh, fl, fd, atr_i = A["fib_hi"][i], A["fib_lo"][i], A["fib_dir"][i], A["atr"][i] + fib_zone, fib_depth = 0, None + if fh == fh and fl == fl and fd == fd and atr_i == atr_i and atr_i > 0 and (fh - fl) >= FIB_MIN_RANGE_ATR * atr_i: + rng = fh - fl + depth = (fh - price) / rng if fd > 0 else (price - fl) / rng + fib_depth = depth + d = 1 if fd > 0 else -1 + bounce = (A["c"][i] > A["o"][i]) if d > 0 else (A["c"][i] < A["o"][i]) + near = min(FIB_RATIOS, key=lambda r: abs(r - depth)) + wave = "yükseliş" if d > 0 else "düşüş" + role = "destek" if d > 0 else "direnç" + if 0.35 <= depth <= 0.65: + fib_zone = d + add("Fibonacci", "F", d * (1.5 if bounce else 0.5), + f"Fibonacci ALTIN BÖLGE: {wave} dalgasının %{depth * 100:.1f} düzeltmesinde (Fib {near:g} yakını) — {role} bölgesi" + + (", mum tepkiyi teyit ediyor" if bounce else ", henüz mum teyidi yok")) + elif 0.236 <= depth < 0.35: + add("Fibonacci", "F", d * 0.5, + f"Sığ düzeltme (%{depth * 100:.1f}, Fib 0.236–0.382) — {wave} dalgası güçlü, trend devamı lehine") + elif 0.65 < depth <= 0.82: + add("Fibonacci", "F", d * (0.5 if bounce else 0.0), + f"Derin düzeltme (%{depth * 100:.1f}, Fib 0.786 yakını) — son {role} bölgesi") + elif depth > 0.82: + add("Fibonacci", "F", -d * 1.0, + f"Fibonacci 0.786 aşıldı (%{depth * 100:.1f}) — {wave} dalgası geçersizleşiyor, ters yön riski") + + # --- RSI UYUMSUZLUĞU (D) — sistemdeki en büyük ağırlık --- + dvb, dvs = A["div_bull"][i], A["div_bear"][i] + if dvb > 0: + add("RSI Uyumsuzluğu", "D", DIV_WEIGHT * dvb, + "RSI YÜKSELİŞ uyumsuzluğu: fiyat daha düşük dip yaptı, RSI daha yüksek dip — en yüksek ağırlıklı faktör" + + (" (aşırı satımdan)" if dvb > 1 else "")) + if dvs > 0: + add("RSI Uyumsuzluğu", "D", -DIV_WEIGHT * dvs, + "RSI DÜŞÜŞ uyumsuzluğu: fiyat daha yüksek tepe yaptı, RSI daha düşük tepe — en yüksek ağırlıklı faktör" + + (" (aşırı alımdan)" if dvs > 1 else "")) + + # --- Hacim ısı haritası (H) --- + poc, vlo, vhi, heat = A["vp_poc"][i], A["vp_val"][i], A["vp_vah"][i], A["vp_heat"][i] + heat_state = 0 + if poc == poc and vlo == vlo and vhi == vhi and heat == heat: + atr_h = A["atr"][i] if A["atr"][i] == A["atr"][i] and A["atr"][i] > 0 else price * 0.005 + if price > vhi: + heat_state = 1 + add("Isı Haritası · Değer Alanı", "H", 1.0, + f"Fiyat hacim değer alanının (VAH {fmt_price(vhi)}) üzerinde — alıcılar yeni değeri kabul ediyor") + elif price < vlo: + heat_state = -1 + add("Isı Haritası · Değer Alanı", "H", -1.0, + f"Fiyat hacim değer alanının (VAL {fmt_price(vlo)}) altında — satıcılar yeni değeri kabul ediyor") + elif abs(price - poc) > 0.3 * atr_h: + d = 1 if price > poc else -1 + add("Isı Haritası · POC", "H", 0.5 * d, + f"Fiyat en sıcak hacim seviyesinin (POC {fmt_price(poc)}) {'üzerinde — POC destek' if d > 0 else 'altında — POC direnç'}") + else: + add("Isı Haritası · POC", "H", 0, f"Fiyat POC ({fmt_price(poc)}) civarında — denge bölgesi, yön belirsiz") + if heat < HEAT_COLD and i >= 3: + mv = price - A["c"][i - 3] + if mv != 0: + d = 1 if mv > 0 else -1 + add("Isı Haritası · Soğuk Bölge", "H", 0.75 * d, + f"Fiyat düşük hacimli 'soğuk' bölgede (yoğunluk %{heat * 100:.0f}) — hareket {'yukarı' if d > 0 else 'aşağı'} hızlı ilerleyebilir") + + # --- Ichimoku (T) --- + sa, sb = A["ich_a"][i], A["ich_b"][i] + if sa == sa and sb == sb: + top, bot = max(sa, sb), min(sa, sb) + if price > top: + add("Ichimoku Bulutu", "T", 1.5, "Fiyat Ichimoku bulutunun üzerinde (boğa bölgesi)") + elif price < bot: + add("Ichimoku Bulutu", "T", -1.5, "Fiyat Ichimoku bulutunun altında (ayı bölgesi)") + else: + add("Ichimoku Bulutu", "T", 0, "Fiyat Ichimoku bulutunun içinde (kararsız bölge)") + tk, kj = A["tenkan"][i], A["kijun"][i] + if tk == tk and kj == kj: + add("Tenkan/Kijun", "T", 0.5 if tk > kj else -0.5, + "Tenkan > Kijun (kısa vadeli momentum yukarı)" if tk > kj else "Tenkan < Kijun (kısa vadeli momentum aşağı)") + + # --- Donchian kırılımı (T) --- + dhi, dlo, rvol = A["dc_hi20"][i], A["dc_lo20"][i], A["rvol"][i] + if dhi == dhi and dlo == dlo: + if price > dhi: + if rvol >= 1.2: + 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)") + else: + add("Donchian Kırılımı", "T", 0.75, f"20 barlık tepe kırıldı ama hacim zayıf ({rvol:.1f}x)") + elif price < dlo: + if rvol >= 1.2: + 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)") + else: + add("Donchian Kırılımı", "T", -0.75, f"20 barlık dip kırıldı ama hacim zayıf ({rvol:.1f}x)") + + # --- Piyasa yapısı (T) --- + st_v = A["struct"][i] + if st_v > 0: + add("Piyasa Yapısı", "T", 1, "Piyasa yapısı: Higher-High / Higher-Low (yükseliş yapısı)") + elif st_v < 0: + add("Piyasa Yapısı", "T", -1, "Piyasa yapısı: Lower-High / Lower-Low (düşüş yapısı)") + + # --- Sıkışma (Bollinger ⊂ Keltner) --- + squeeze_on = A["sq_on"][i] > 0 + mom = A["sq_mom"][i] + if A["sq_rel3"][i] > 0 and mom == mom and mom != 0: + d = 1 if mom > 0 else -1 + add("Sıkışma Kırılımı", "T", 1.5 * d, + "Volatilite sıkışması yukarı yönde çözüldü" if d > 0 else "Volatilite sıkışması aşağı yönde çözüldü") + elif squeeze_on: + add("Sıkışma", "T", 0, "Volatilite sıkışması aktif — kırılım bekleniyor, sinyal güveni düşürüldü") + + # --- REJİM (ADX) — sürekli ağırlıklandırma --- + adx_v = A["adx"][i] + 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 + trend_w = 0.5 + 0.8 * w + rev_w = 1.3 - 0.95 * w + di_dir = 1 if A["plus_di"][i] > A["minus_di"][i] else -1 + add("ADX / DI Yönü", "T", di_dir * w, "") # yönsel bonus; w kadar ölçeklenir + if w >= 0.67: + regime = "TREND" + 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ı") + elif w <= 0.33: + regime = "YATAY" + add("Rejim", "T", 0, f"ADX zayıf/yatay piyasa ({adx_v:.1f}) — ortalamaya dönüş faktörleri ağırlıklı") + else: + regime = "GEÇİŞ" + add("Rejim", "T", 0, f"ADX geçiş bölgesinde ({adx_v:.1f}) — faktör ağırlıkları dengeli karışım") + + gw = {"T": trend_w, "R": rev_w} # "F" (Fibonacci) rejimden bağımsız: ağırlık 1.0 + contribs = [(lab, gw.get(g, 1.0) * raw, raw) for lab, g, raw, _ in items] + raw_total = sum(c for _, c, _ in contribs) + if squeeze_on: + raw_total *= 0.75 + score = 10.0 * math.tanh(raw_total / 10.0) # 2 yeni faktör eklendiği için ölçek 9 -> 10 + bull_w = sum(c for _, c, _ in contribs if c > 0) + bear_w = -sum(c for _, c, _ in contribs if c < 0) + + out = { + "score": score, "price": price, "adx": adx_v, "w": w, "regime": regime, + "bull_w": bull_w, "bear_w": bear_w, "squeeze_on": bool(squeeze_on), + "atr_rank": A["atr_rank"][i], "pat_hits": hits, + "hilo_flip": hilo_flip, "fib_zone": fib_zone, "fib_depth": fib_depth, + "heat_state": heat_state, "heat": heat, "div_bull": dvb, "div_bear": dvs, + } + if verbose: + def tone(raw): + return "up" if raw > 0 else ("down" if raw < 0 else "flat") + out["reasons"] = [(tone(raw), text) for _, _, raw, text in items if text] + out["contribs"] = [(lab, c) for lab, c, raw in contribs if abs(c) > 1e-9] + return out + + +def compute_score_series(ind, n): + """Son BACKTEST_LOOKBACK bar için (canlı bar dahil) skor serisi. Dönüş: (başlangıç_indeksi, [skor_dict,...])""" + start = min(max(WARMUP_BARS, n - BACKTEST_LOOKBACK), n - 1) + return start, [_score_at(ind, i) for i in range(start, n)] + + +# --------------------------------------------------------------------------- +# BACKTEST — yön doğruluğu + gerçek işlem simülasyonu (SL/TP/zaman çıkışı + komisyon) +# --------------------------------------------------------------------------- +def directional_accuracy(df, start, scores, forward=5, threshold=SIGNAL_THRESHOLD): + """Klasik test: sinyal 'forward' bar sonra doğru yönü göstermiş mi? (rejim kırılımlı)""" + c = df["c"].to_numpy() + n = len(c) + stats = {"all": [0, 0], "trend": [0, 0], "range": [0, 0]} + for i in range(start, n - forward): + s = scores[i - start] + if abs(s["score"]) < threshold: + continue + correct = (s["score"] > 0) == (c[i + forward] > c[i]) + regime = "trend" if s["adx"] >= 25 else "range" + for key in ("all", regime): + stats[key][1] += 1 + if correct: + stats[key][0] += 1 + if stats["all"][1] < 5: + return None + + def pack(key): + ok, t = stats[key] + return {"accuracy": ok / t, "n_signals": t} if t > 0 else None + + return { + "accuracy": stats["all"][0] / stats["all"][1], "correct": stats["all"][0], + "n_signals": stats["all"][1], "forward_bars": forward, + "trend_regime": pack("trend"), "range_regime": pack("range"), + } + + +def simulate_trades(df, ind, start, scores, threshold=SIGNAL_THRESHOLD): + """Sinyalleri sırayla (üst üste binmeden) işleme çevirir. + Giriş: sinyal barını izleyen barın AÇILIŞI (gerçekçi, ileri sızıntı yok). + Çıkış: SL (1.5×ATR) / TP (2.5×ATR) / 24 bar zaman çıkışı. Aynı barda ikisi de değerse SL öncelikli (kötümser).""" + A = ind["_arr"] + o, h, l, c, atr_a = A["o"], A["h"], A["l"], A["c"], A["atr"] + n = len(c) + trades = [] + i = start + while i <= n - 2: + s = scores[i - start]["score"] + a = atr_a[i] + if abs(s) < threshold or not (a > 0): + i += 1 + continue + d = 1 if s > 0 else -1 + entry = o[i + 1] + risk = SL_ATR * a + sl, tp = entry - d * risk, entry + d * TP_ATR * a + last = min(n - 1, i + 1 + MAX_HOLD_BARS) + exit_i, exit_px, why = None, None, None + for j in range(i + 1, last + 1): + hit_sl = (l[j] <= sl) if d > 0 else (h[j] >= sl) + hit_tp = (h[j] >= tp) if d > 0 else (l[j] <= tp) + if hit_sl: + exit_i, exit_px, why = j, sl, "SL" + break + if hit_tp: + exit_i, exit_px, why = j, tp, "TP" + break + if exit_i is None: + if last == i + 1 + MAX_HOLD_BARS: + exit_i, exit_px, why = last, c[last], "ZAMAN" + else: + break # veri bitti, işlem hâlâ açık — sonuçlandırılamaz + pnl = d * (exit_px - entry) / entry - 2 * FEE_PER_SIDE + r_mult = pnl / (risk / entry) + trades.append({"signal_i": i, "entry_i": i + 1, "exit_i": exit_i, "dir": d, "entry": entry, + "exit": exit_px, "pnl": pnl, "r": r_mult, "why": why, "score": s}) + i = exit_i + return trades + + +def summarize_trades(trades, df, start): + if len(trades) < 3: + return None + n = len(df) + pnl = np.array([t["pnl"] for t in trades]) + r = np.array([t["r"] for t in trades]) + wins, losses = pnl[pnl > 0], pnl[pnl <= 0] + gross_loss = abs(losses.sum()) + pf = float(wins.sum() / gross_loss) if gross_loss > 0 else (99.0 if wins.sum() > 0 else 0.0) + win_rate = len(wins) / len(pnl) + win_r, loss_r = r[r > 0], r[r <= 0] + avg_win_r = float(win_r.mean()) if len(win_r) else 0.0 + avg_loss_r = float(abs(loss_r.mean())) if len(loss_r) else 0.0 + kelly = None + if avg_win_r > 0 and avg_loss_r > 0: + kelly = win_rate - (1 - win_rate) / (avg_win_r / avg_loss_r) + sqn = float(np.sqrt(min(len(r), 100)) * r.mean() / r.std()) if r.std() > 0 else 0.0 + + mult = np.ones(n - start) + for t in trades: + mult[t["exit_i"] - start] *= (1 + t["pnl"]) + equity = np.cumprod(mult) + peak = np.maximum.accumulate(equity) + max_dd = float(((equity - peak) / peak).min()) + c = df["c"].to_numpy() + bh = c[start:] / c[start] + + streak = best = 0 + for p in pnl: + streak = streak + 1 if p <= 0 else 0 + best = max(best, streak) + + def side(dirn): + sub = [t for t in trades if t["dir"] == dirn] + return {"n": len(sub), "win_rate": (sum(1 for t in sub if t["pnl"] > 0) / len(sub)) if sub else None} + + stats = { + "n": len(trades), "win_rate": win_rate, "profit_factor": pf, "expectancy": float(pnl.mean()), + "avg_r": float(r.mean()), "sqn": sqn, "max_dd": max_dd, "total_return": float(equity[-1] - 1), + "bh_return": float(bh[-1] - 1), "max_consec_loss": best, "avg_win_r": avg_win_r, + "avg_loss_r": avg_loss_r, "kelly": kelly, "long": side(1), "short": side(-1), + } + return stats, equity, bh + + +def run_backtest(df, ind, start, scores): + n = len(df) + if n - start < 20: + return None + acc = directional_accuracy(df, start, scores) + trades = simulate_trades(df, ind, start, scores) + summ = summarize_trades(trades, df, start) + bt = {"start": start, "acc": acc, "trades": trades, "stats": None, "equity": None, "bh": None} + if summ: + bt["stats"], bt["equity"], bt["bh"] = summ + return bt + + +# --------------------------------------------------------------------------- +# CANLI KATMAN — üst zaman dilimleri, emir defteri, funding (backtest EDİLMEZ) +# --------------------------------------------------------------------------- +def _mtf_score(symbol, bar, closed_only): + hdf = fetch_candles(symbol, bar, 300) + if closed_only: + hdf = hdf[hdf["confirm"] == 1].reset_index(drop=True) + hind = compute_indicators(hdf) + r = _score_at(hind, len(hdf) - 1) + return {"bar": bar, "score": r["score"]} + + +def _fetch_live_extras(symbol, bar, mtf, micro, closed_only): + """MTF / emir defteri / funding çağrılarını PARALEL yapar; hata veren parça sessizce atlanır.""" + tasks = {} + with ThreadPoolExecutor(max_workers=4) as ex: + if mtf and symbol and bar: + b1 = MTF_MAP.get(bar) + if b1 and b1 != bar: + tasks["mtf1"] = ex.submit(_mtf_score, symbol, b1, closed_only) + b2 = MTF_MAP.get(b1) + if b2 and b2 not in (b1, bar): + tasks["mtf2"] = ex.submit(_mtf_score, symbol, b2, closed_only) + if micro and symbol: + tasks["ob"] = ex.submit(fetch_orderbook, symbol, 20) + tasks["fund"] = ex.submit(fetch_funding_rate, symbol) + out = {} + for k, f in tasks.items(): + try: + out[k] = f.result() + except Exception: + out[k] = None + return out + + +def build_trade_plan(price, direction, atr_v, supports, resists, acct=None, risk_pct=None): + """Yapısal (destek/direnç) stop uygun aralıktaysa onu, değilse ATR stop'unu kullanır. + 3 kademeli hedef + hesap büyüklüğüne göre pozisyon boyutu.""" + d = direction + stop, stop_type = price - d * SL_ATR * atr_v, f"ATR × {SL_ATR}" + cand = None + if d > 0 and supports: + cand = supports[0][0] - 0.25 * atr_v + elif d < 0 and resists: + cand = resists[0][0] + 0.25 * atr_v + if cand is not None: + dist = abs(price - cand) + if 0.8 * atr_v <= dist <= 3.0 * atr_v and (cand < price if d > 0 else cand > price): + stop, stop_type = cand, "Yapısal (S/R arkası)" + risk = abs(price - stop) + plan = { + "direction": "LONG" if d > 0 else "SHORT", "entry": price, "stop_loss": stop, "stop_type": stop_type, + "risk_dist": risk, "risk_frac": risk / price, + "targets": [(price + d * risk * m, m) for m in TP_R_MULTIPLES], "sizing": None, + } + if acct and acct > 0 and risk_pct and risk_pct > 0 and risk > 0: + risk_amt = acct * risk_pct / 100.0 + qty = risk_amt / risk + notional = qty * price + plan["sizing"] = {"risk_amount": risk_amt, "qty": qty, "notional": notional, "leverage": notional / acct} + return plan + + +def compute_confidence(score, agree, mtf_state, hist_q, vol_q): + mag = min(abs(score) / 8.0, 1.0) + conf = 100.0 * (0.35 * mag + 0.25 * agree + 0.15 * mtf_state + 0.20 * hist_q + 0.05 * vol_q) + parts = {"Skor Büyüklüğü": mag, "Faktör Uyumu": agree, "MTF Teyidi": mtf_state, + "Tarihsel Kenar": hist_q, "Volatilite Rejimi": vol_q} + return min(conf, 100.0), parts + + +def grade_from_conf(conf): + for thr, g, col in ((75, "A+", "#3ddc97"), (62, "A", "#7fe6b8"), (50, "B", "#f2b84b"), (38, "C", "#ff9aa8")): + if conf >= thr: + return g, col + return "D", "#ff5c72" + + +def generate_signal(df, symbol=None, bar=None, mtf=True, micro=True, closed_only=False, + acct=None, risk_pct=None): + ind = compute_indicators(df) + n = len(df) + start, scores = compute_score_series(ind, n) + base = _score_at(ind, n - 1, verbose=True) + score, price, w = base["score"], base["price"], base["w"] + reasons, contribs = list(base["reasons"]), list(base["contribs"]) + tags = [] + if base["squeeze_on"]: + tags.append(("SIKIŞMA AKTİF", "#f2b84b")) + for name, sgn in base["pat_hits"]: + tags.append((name.upper(), "#3ddc97" if sgn > 0 else "#ff5c72")) + if base["hilo_flip"]: + tags.append(("HiLo YUKARI DÖNÜŞ" if base["hilo_flip"] > 0 else "HiLo AŞAĞI DÖNÜŞ", + "#3ddc97" if base["hilo_flip"] > 0 else "#ff5c72")) + if base["heat_state"]: + tags.append(("DEĞER ALANI ÜSTÜ" if base["heat_state"] > 0 else "DEĞER ALANI ALTI", + "#3ddc97" if base["heat_state"] > 0 else "#ff5c72")) + if base["fib_zone"]: + tags.append(("FİB ALTIN BÖLGE ▲" if base["fib_zone"] > 0 else "FİB ALTIN BÖLGE ▼", + "#3ddc97" if base["fib_zone"] > 0 else "#ff5c72")) + + def overlay(label, delta, text): + nonlocal score + score += delta + reasons.append(("up" if delta > 0 else "down", text)) + contribs.append((label, delta)) + + # --- Uyumsuzluk (RSI divergence): artık _score_at içinde, en yüksek ağırlıklı faktör olarak hesaplanır --- + div = {"bullish": base["div_bull"] > 0, "bearish": base["div_bear"] > 0} + if div["bullish"]: + tags.append(("YÜKSELİŞ UYUMSUZLUĞU", "#3ddc97")) + if div["bearish"]: + tags.append(("DÜŞÜŞ UYUMSUZLUĞU", "#ff5c72")) + + # --- Skor ivmesi --- + if n - 4 >= start: + momentum = base["score"] - scores[n - 4 - start]["score"] + if momentum > 1.5: + overlay("Skor İvmesi", 0.3, "Kompozit skor son barlarda hızla güçleniyor") + elif momentum < -1.5: + overlay("Skor İvmesi", -0.3, "Kompozit skor son barlarda hızla zayıflıyor") + + # --- Canlı ekstralar (paralel çekim) --- + extras = _fetch_live_extras(symbol, bar, mtf, micro, closed_only) + sgn = 1 if score > 0 else (-1 if score < 0 else 0) + + # --- 2 kademeli MTF teyidi --- + mtf_info, align_flags = [], [] + for key, wt in (("mtf1", 1.0), ("mtf2", 0.6)): + m = extras.get(key) + if not m: + continue + hs = m["score"] + aligned = None + if abs(hs) >= 1.0 and sgn != 0: + aligned = bool(np.sign(hs) == sgn) + align_flags.append(aligned) + if aligned: + overlay(f"{m['bar']} Teyidi", sgn * wt, f"{m['bar']} zaman diliminde de aynı yönlü teyit var (skor {hs:+.1f})") + else: + overlay(f"{m['bar']} Çelişkisi", -sgn * wt, f"{m['bar']} zaman dilimi ters yönlü sinyal veriyor (skor {hs:+.1f}) — dikkat") + mtf_info.append({"bar": m["bar"], "score": round(hs, 2), "aligned": aligned}) + mtf_state = (sum(1.0 for a in align_flags if a) / len(align_flags)) if align_flags else 0.5 + mtf_opposed = any(a is False for a in align_flags) + + # --- Emir defteri dengesizliği (derinlik ağırlıklı, kısa vadede anlamlı) --- + imb = None + ob = extras.get("ob") + if ob and ob.get("bids") and ob.get("asks"): + bw = sum(float(x[1]) / (k + 1) for k, x in enumerate(ob["bids"])) + aw = sum(float(x[1]) / (k + 1) for k, x in enumerate(ob["asks"])) + if bw + aw > 0: + imb = (bw - aw) / (bw + aw) + if abs(imb) >= 0.2: + delta = max(-0.8, min(0.8, imb * 2)) * (0.5 if bar in SLOW_BARS else 1.0) + overlay("Emir Defteri", delta, + f"Emir defterinde {'alış' if imb > 0 else 'satış'} baskısı (dengesizlik {imb * 100:+.0f}%)") + + # --- Likidite ısı haritası: emir defteri duvarları --- + walls = None + atr_w = float(ind["atr"].iloc[-1]) + if ob and ob.get("bids") and ob.get("asks") and atr_w > 0: + sup_w, res_w = book_walls(ob, price, atr_w) + walls = {"support": sup_w, "resist": res_w} + slow = 0.5 if bar in SLOW_BARS else 1.0 + if sup_w: + overlay("Likidite Duvarı (Alış)", 0.5 * slow, + f"Yakında büyük alış duvarı {fmt_price(sup_w[0])} (medyanın {sup_w[1]:.1f}×'i) — destek olabilir") + if res_w: + overlay("Likidite Duvarı (Satış)", -0.5 * slow, + f"Yakında büyük satış duvarı {fmt_price(res_w[0])} (medyanın {res_w[1]:.1f}×'i) — direnç olabilir") + + # --- Funding rate (kalabalık pozisyon = kontraryen) --- + fr = extras.get("fund") + if fr is not None: + if fr > 0.0005: + overlay("Funding", -0.6, f"Funding aşırı pozitif ({fr * 100:.3f}%) — kalabalık long, tasfiye riski") + elif fr > 0.0002: + overlay("Funding", -0.3, f"Funding yüksek pozitif ({fr * 100:.3f}%) — long tarafı kalabalık") + elif fr < -0.0005: + overlay("Funding", 0.6, f"Funding aşırı negatif ({fr * 100:.3f}%) — kalabalık short, sıkışma potansiyeli") + elif fr < -0.0002: + overlay("Funding", 0.3, f"Funding negatif ({fr * 100:.3f}%) — short tarafı kalabalık") + + # --- Destek / direnç yakınlığı --- + atr_v = float(ind["atr"].iloc[-1]) + supports, resists = find_sr_levels(df, atr_v) + fib = None + _fh, _fl, _fd = (float(ind["fib_hi"].iloc[-1]), float(ind["fib_lo"].iloc[-1]), float(ind["fib_dir"].iloc[-1])) + if _fh == _fh and _fl == _fl and _fd == _fd and atr_v > 0 and (_fh - _fl) >= FIB_MIN_RANGE_ATR * atr_v: + fib = {"hi": _fh, "lo": _fl, "dir": int(_fd), "levels": fib_levels(_fh, _fl, _fd), "depth": base["fib_depth"]} + sgn = 1 if score > 0 else (-1 if score < 0 else 0) + if atr_v > 0 and sgn != 0: + if sgn > 0: + if resists and resists[0][1] >= 2 and (resists[0][0] - price) / atr_v <= 1.0: + lv, tc = resists[0] + 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") + elif supports and supports[0][1] >= 2 and (price - supports[0][0]) / atr_v <= 0.5: + lv, tc = supports[0] + overlay("Destek Üstünde", 0.4, f"Fiyat güçlü destek {fmt_price(lv)} ({tc} temas) üzerinde") + else: + if supports and supports[0][1] >= 2 and (price - supports[0][0]) / atr_v <= 1.0: + lv, tc = supports[0] + 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") + elif resists and resists[0][1] >= 2 and (resists[0][0] - price) / atr_v <= 0.5: + lv, tc = resists[0] + overlay("Direnç Altında", -0.4, f"Fiyat güçlü direnç {fmt_price(lv)} ({tc} temas) altında") + + score = max(-10.0, min(10.0, score)) + + # --- Konsensüs + sınıflandırma --- + direction = 1 if score > 0 else -1 + tot = base["bull_w"] + base["bear_w"] + agree = ((base["bull_w"] if direction > 0 else base["bear_w"]) / tot) if tot > 0 else 0.5 + if abs(score) >= STRONG_THRESHOLD and agree >= 0.65 and not mtf_opposed: + signal, color = ("GÜÇLÜ AL", "#3ddc97") if score > 0 else ("GÜÇLÜ SAT", "#ff5c72") + elif abs(score) >= SIGNAL_THRESHOLD: + signal, color = ("AL", "#7fe6b8") if score > 0 else ("SAT", "#ff9aa8") + if abs(score) >= STRONG_THRESHOLD: + 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")) + else: + signal, color = "BEKLE / NÖTR", "#f2b84b" + is_neutral = signal == "BEKLE / NÖTR" + + # --- Tarihsel doğrulama --- + backtest = run_backtest(df, ind, start, scores) + hist_q = 0.4 + if backtest: + qs = [] + acc = backtest["acc"] + if acc: + p_shrunk = (acc["correct"] + 5) / (acc["n_signals"] + 10) # Bayes büzülmesi (önsel %50) + qs.append(min(max((p_shrunk - 0.45) / 0.20, 0.0), 1.0)) + st = backtest["stats"] + if st: + qs.append(min(max((st["profit_factor"] - 0.8) / 0.8, 0.0), 1.0)) + if qs: + hist_q = float(np.mean(qs)) + rank = base["atr_rank"] + vol_q = 1.0 if 0.10 <= rank <= 0.90 else 0.5 + if rank > 0.90: + reasons.append(("flat", f"Volatilite aşırı yüksek (ATR yüzdeliği %{rank * 100:.0f}) — stoplar geniş tutulmalı, güven düşürüldü")) + conf, conf_parts = compute_confidence(score, agree, mtf_state, hist_q, vol_q) + grade, grade_color = ("—", "#7c8797") if is_neutral else grade_from_conf(conf) + + plan = None + if not is_neutral and atr_v > 0: + plan = build_trade_plan(price, direction, atr_v, supports, resists, acct, risk_pct) + + return { + "signal": signal, "score": round(score, 2), "confidence": round(conf, 1), "grade": grade, + "grade_color": grade_color, "color": color, "reasons": reasons, "contribs": contribs, + "indicators": ind, "price": price, "divergence": div, "mtf": mtf_info, "plan": plan, + "backtest": backtest, "regime": base["regime"], "adx": base["adx"], "atr_rank": rank, + "squeeze_on": base["squeeze_on"], "agree": agree, "bull_w": base["bull_w"], "bear_w": base["bear_w"], + "conf_parts": conf_parts, "tags": tags, "funding": fr, "imbalance": imb, + "supports": supports, "resists": resists, "atr": atr_v, "fib": fib, "walls": walls, + "vp": {"poc": float(ind["vp_poc"].iloc[-1]), "val": float(ind["vp_val"].iloc[-1]), + "vah": float(ind["vp_vah"].iloc[-1]), "heat": float(ind["vp_heat"].iloc[-1])}, + "hilo_dir": int(ind["hilo_dir"].iloc[-1]), + } + + +# --------------------------------------------------------------------------- +# RISK ANALYTICS +# --------------------------------------------------------------------------- +def risk_metrics(df, bar): + ret = df["c"].pct_change().dropna() + if len(ret) < 5: + return None + periods = BAR_TO_PERIODS_PER_YEAR.get(bar, 365) + vol_annual = ret.std() * np.sqrt(periods) + mean_annual = ret.mean() * periods + sharpe = mean_annual / vol_annual if vol_annual > 0 else 0.0 + cum = (1 + ret).cumprod() + peak = cum.cummax() + dd = (cum - peak) / peak + var95 = np.percentile(ret, 5) + tail = ret[ret <= var95] + cvar95 = tail.mean() if len(tail) else var95 + return { + "volatility_annual": vol_annual, "return_annual": mean_annual, "sharpe": sharpe, + "max_drawdown": dd.min(), "current_dd": dd.iloc[-1], + "var95_bar": var95, "cvar95_bar": cvar95, + } + + +# --------------------------------------------------------------------------- +# FORMAT HELPERS +# --------------------------------------------------------------------------- +def _isnan(v): + return v is None or (isinstance(v, float) and math.isnan(v)) + + +def fmt_price(v): + if _isnan(v): + return "—" + av = abs(v) + if av >= 1000: + return f"{v:,.2f}" + if av >= 1: + return f"{v:,.4f}" + if av >= 0.01: + return f"{v:.6f}" + return f"{v:.8f}" + + +def fmt_pct(v): + if _isnan(v): + return "—" + sign = "+" if v > 0 else "" + return f"{sign}{v * 100:.2f}%" + + +def fmt_large(v): + if _isnan(v): + return "—" + av = abs(v) + if av >= 1e9: + return f"{v / 1e9:.2f}B" + if av >= 1e6: + return f"{v / 1e6:.2f}M" + if av >= 1e3: + return f"{v / 1e3:.2f}K" + return f"{v:.2f}" + + +def metric_card(label, value, sub="", tone="neutral"): + tone_class = {"up": "mc-up", "down": "mc-down", "neutral": "mc-neutral"}.get(tone, "mc-neutral") + sub_html = f'
{sub}
' if sub else "" + return (f'
{label}
' + f'
{value}
{sub_html}
') + + +def section(title): + return f'
{title}
' + + +def cards_row(cards): + return f'
{"".join(cards)}
' if cards else "" + + +def error_box(msg): + return f'
⚠ {msg}
' + + +# --------------------------------------------------------------------------- +# CHART BUILDERS +# --------------------------------------------------------------------------- +def _base_layout(fig, title, height): + fig.update_layout( + template="plotly_dark", height=height, margin=dict(l=10, r=10, t=40, b=10), + paper_bgcolor=CHART_BG, plot_bgcolor=CHART_BG, title=title, + legend=dict(orientation="h", y=1.05, font=dict(size=10)), + font=dict(family="IBM Plex Mono, monospace", size=11), + ) + return fig + + +def _empty_fig(msg, title="", height=220): + fig = go.Figure() + fig.add_annotation(text=msg, x=0.5, y=0.5, xref="paper", yref="paper", showarrow=False, + font=dict(size=13, color="#7c8797")) + fig.update_xaxes(visible=False) + fig.update_yaxes(visible=False) + return _base_layout(fig, title, height) + + +def build_price_chart(df, ind, symbol, bar): + fig = make_subplots(rows=2, cols=1, shared_xaxes=True, row_heights=[0.75, 0.25], vertical_spacing=0.03) + fig.add_trace(go.Candlestick( + x=df["ts"], open=df["o"], high=df["h"], low=df["l"], close=df["c"], name=symbol, + increasing_line_color="#3ddc97", decreasing_line_color="#ff5c72"), row=1, col=1) + fig.add_trace(go.Scatter(x=df["ts"], y=ind["ema20"], name="EMA20", + line=dict(color="#5b8cff", width=1)), row=1, col=1) + fig.add_trace(go.Scatter(x=df["ts"], y=ind["ema50"], name="EMA50", + line=dict(color="#f2b84b", width=1)), row=1, col=1) + fig.add_trace(go.Scatter(x=df["ts"], y=ind["bb_upper"], name="BB Üst", + line=dict(color="rgba(150,160,255,0.45)", width=1, dash="dot")), row=1, col=1) + fig.add_trace(go.Scatter(x=df["ts"], y=ind["bb_lower"], name="BB Alt", + line=dict(color="rgba(150,160,255,0.45)", width=1, dash="dot"), + fill="tonexty", fillcolor="rgba(150,160,255,0.05)"), row=1, col=1) + vol_colors = np.where(df["c"] >= df["o"], "#3ddc97", "#ff5c72") + fig.add_trace(go.Bar(x=df["ts"], y=df["vol"], name="Hacim", marker_color=vol_colors, opacity=0.55), + row=2, col=1) + fig.update_layout(xaxis_rangeslider_visible=False) + return _base_layout(fig, f"{symbol} · {bar}", 520) + + +def build_signal_price_chart(df, ind, symbol, bar, res): + """Sinyal sekmesinin ana grafiği: hacim ısı haritası + mum + Supertrend + HiLo + Fibonacci + Ichimoku bulutu + S/R + geçmiş sinyal işaretleri + işlem planı.""" + n = len(df) + v0 = max(0, n - 200) + sl = slice(v0, n) + ts = df["ts"].iloc[sl] + fig = go.Figure() + # Hacim ısı haritası (zaman × fiyat): her barın hacmi kapsadığı fiyat dilimlerine yayılır + try: + hh, ll = df["h"].to_numpy(dtype=float)[sl], df["l"].to_numpy(dtype=float)[sl] + edges = np.linspace(ll.min(), hh.max(), 61) + z = _volume_grid(hh, ll, df["vol"].to_numpy(dtype=float)[sl], edges).T # (dilim × bar) + z = z / max(z.max(), 1e-12) + fig.add_trace(go.Heatmap(x=ts, y=(edges[:-1] + edges[1:]) / 2, z=z, name="Hacim Isı Haritası", + colorscale=[[0, "rgba(10,14,20,0)"], [0.35, "rgba(91,140,255,0.18)"], + [0.7, "rgba(242,184,75,0.35)"], [1, "rgba(255,92,114,0.55)"]], + showscale=False, hoverinfo="skip", zsmooth="best")) + except Exception: + pass + fig.add_trace(go.Candlestick( + x=ts, open=df["o"].iloc[sl], high=df["h"].iloc[sl], low=df["l"].iloc[sl], close=df["c"].iloc[sl], + name=symbol, increasing_line_color="#3ddc97", decreasing_line_color="#ff5c72")) + vp = res.get("vp") + if vp and vp["poc"] == vp["poc"]: + for lab, y, col in (("POC", vp["poc"], "#f2b84b"), ("VAH", vp["vah"], "rgba(242,184,75,0.6)"), ("VAL", vp["val"], "rgba(242,184,75,0.6)")): + fig.add_hline(y=y, line=dict(color=col, width=1.4 if lab == "POC" else 0.8, dash="solid" if lab == "POC" else "dot"), + annotation_text=f"{lab} {fmt_price(y)}", annotation_position="right", + annotation_font=dict(size=9, color="#f2b84b")) + for key, col, nm in (("support", "#3ddc97", "Alış Duvarı"), ("resist", "#ff5c72", "Satış Duvarı")): + w_ = (res.get("walls") or {}).get(key) + if w_: + fig.add_hline(y=w_[0], line=dict(color=col, width=3, dash="solid"), opacity=0.35, + annotation_text=f"{nm} {fmt_price(w_[0])} ×{w_[1]:.1f}", annotation_position="right", + annotation_font=dict(size=9, color=col)) + # Ichimoku bulutu + fig.add_trace(go.Scatter(x=ts, y=ind["ich_a"].iloc[sl], line=dict(width=0), showlegend=False, hoverinfo="skip")) + fig.add_trace(go.Scatter(x=ts, y=ind["ich_b"].iloc[sl], line=dict(width=0), name="Ichimoku Bulutu", + fill="tonexty", fillcolor="rgba(120,140,255,0.10)", hoverinfo="skip")) + # Supertrend (yükseliş/düşüş ayrı renk) + st_line, st_dir = ind["st_line"].iloc[sl], ind["st_dir"].iloc[sl] + fig.add_trace(go.Scatter(x=ts, y=st_line.where(st_dir > 0), name="Supertrend ▲", + line=dict(color="#3ddc97", width=1.5), connectgaps=False)) + fig.add_trace(go.Scatter(x=ts, y=st_line.where(st_dir < 0), name="Supertrend ▼", + line=dict(color="#ff5c72", width=1.5), connectgaps=False)) + # Gann HiLo Activator (yükseliş/düşüş ayrı renk) + hl_line, hl_dir = ind["hilo_line"].iloc[sl], ind["hilo_dir"].iloc[sl] + fig.add_trace(go.Scatter(x=ts, y=hl_line.where(hl_dir > 0), name="HiLo ▲", mode="lines", + line=dict(color="#7fe6b8", width=1.2, dash="dot", shape="hv"), connectgaps=False)) + fig.add_trace(go.Scatter(x=ts, y=hl_line.where(hl_dir < 0), name="HiLo ▼", mode="lines", + line=dict(color="#ff9aa8", width=1.2, dash="dot", shape="hv"), connectgaps=False)) + # Fibonacci düzeltme seviyeleri + fib = res.get("fib") + if fib: + for r, px in fib["levels"]: + key = r in (0.382, 0.5, 0.618) + fig.add_hline(y=px, line=dict(color="rgba(242,184,75,%s)" % ("0.65" if key else "0.3"), width=1 if key else 0.7, dash="dashdot"), + annotation_text=f"Fib {r:g} · {fmt_price(px)}", annotation_position="right", + annotation_font=dict(size=9, color="#f2b84b")) + # Destek / direnç + for lv, tc in res["supports"][:3]: + fig.add_hline(y=lv, line=dict(color="rgba(61,220,151,0.55)", dash="dash", width=1), + annotation_text=f"D {fmt_price(lv)} ×{tc}", annotation_position="left", + annotation_font=dict(size=9, color="#3ddc97")) + for lv, tc in res["resists"][:3]: + fig.add_hline(y=lv, line=dict(color="rgba(255,92,114,0.55)", dash="dash", width=1), + annotation_text=f"R {fmt_price(lv)} ×{tc}", annotation_position="left", + annotation_font=dict(size=9, color="#ff5c72")) + # Geçmiş sinyal işlemleri + bt = res["backtest"] + if bt and bt["trades"]: + atr_a = ind["_arr"]["atr"] + for d, sym_, col, nm in ((1, "triangle-up", "#3ddc97", "Geçmiş LONG"), (-1, "triangle-down", "#ff5c72", "Geçmiş SHORT")): + tr = [t for t in bt["trades"] if t["dir"] == d and t["entry_i"] >= v0] + if not tr: + continue + xs = [df["ts"].iloc[t["entry_i"]] for t in tr] + ys = [(df["l"].iloc[t["entry_i"]] - 0.6 * atr_a[t["entry_i"]]) if d > 0 + else (df["h"].iloc[t["entry_i"]] + 0.6 * atr_a[t["entry_i"]]) for t in tr] + hov = [f"{nm} · skor {t['score']:+.1f}
Sonuç: {t['why']} ({t['pnl'] * 100:+.2f}% · {t['r']:+.2f}R)" for t in tr] + fig.add_trace(go.Scatter(x=xs, y=ys, mode="markers", name=nm, text=hov, hoverinfo="text", + marker=dict(symbol=sym_, size=9, color=col, line=dict(width=0.5, color="#0a0e14")))) + # Güncel işlem planı + plan = res["plan"] + if plan: + x0, x1 = df["ts"].iloc[max(0, n - 30)], df["ts"].iloc[-1] + lines = [("GİRİŞ", plan["entry"], "#e6ebf2"), ("SL", plan["stop_loss"], "#ff5c72")] + lines += [(f"TP{k + 1}", px, "#3ddc97") for k, (px, _) in enumerate(plan["targets"])] + for lab, y, col in lines: + fig.add_shape(type="line", x0=x0, x1=x1, y0=y, y1=y, line=dict(color=col, width=1.4, dash="dot")) + fig.add_annotation(x=x1, y=y, text=f"{lab} {fmt_price(y)}", showarrow=False, xanchor="left", + font=dict(size=9, color=col)) + fig.update_layout(xaxis_rangeslider_visible=False) + return _base_layout(fig, f"{symbol} · {bar} · Sinyal Haritası", 560) + + +def build_factor_chart(contribs, symbol): + items = [(l, v) for l, v in contribs if abs(v) > 1e-9] + if not items: + return _empty_fig("Faktör verisi yok", f"{symbol} · Faktör Katkıları") + items.sort(key=lambda x: x[1]) + labels, vals = [l for l, _ in items], [v for _, v in items] + fig = go.Figure(go.Bar( + x=vals, y=labels, orientation="h", marker_color=["#3ddc97" if v > 0 else "#ff5c72" for v in vals], + text=[f"{v:+.2f}" for v in vals], textposition="outside", cliponaxis=False)) + fig.update_layout(showlegend=False, xaxis_title="Ağırlıklı katkı (rejim ağırlığı uygulanmış)") + return _base_layout(fig, f"{symbol} · Faktör Katkıları (skoru ne yönlendiriyor?)", max(300, 25 * len(items) + 100)) + + +def build_backtest_chart(bt, df, symbol): + if not bt or not bt.get("stats"): + return _empty_fig("Backtest için yeterli işlem oluşmadı (en az 3 işlem gerekir)", f"{symbol} · Sinyal Backtesti") + start = bt["start"] + ts = df["ts"].iloc[start:] + fig = make_subplots(rows=2, cols=1, row_heights=[0.62, 0.38], vertical_spacing=0.14, + subplot_titles=("Sinyal Stratejisi vs Al&Tut (komisyon dahil, tam pozisyon)", "İşlem Bazında R Çarpanı")) + fig.add_trace(go.Scatter(x=ts, y=bt["equity"], name="Sinyal Stratejisi", line=dict(color="#5b8cff", width=2, shape="hv")), + row=1, col=1) + 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) + rs = [t["r"] for t in bt["trades"]] + fig.add_trace(go.Bar(x=list(range(1, len(rs) + 1)), y=rs, name="R", + marker_color=["#3ddc97" if r > 0 else "#ff5c72" for r in rs], + text=[t["why"] for t in bt["trades"]], hoverinfo="text+y"), row=2, col=1) + fig.update_xaxes(title_text="İşlem #", row=2, col=1) + return _base_layout(fig, f"{symbol} · Walk-Forward İşlem Simülasyonu", 470) + + +def build_signal_chart(df, ind, symbol): + fig = make_subplots(rows=4, cols=1, shared_xaxes=True, row_heights=[0.27, 0.25, 0.24, 0.24], + vertical_spacing=0.05, + subplot_titles=("RSI (14)", "MACD (12,26,9)", "Stokastik (14,3)", "ADX / DI (14)")) + fig.add_trace(go.Scatter(x=df["ts"], y=ind["rsi"], name="RSI", line=dict(color="#5b8cff")), row=1, col=1) + fig.add_hline(y=70, line=dict(color="#ff5c72", dash="dot", width=1), row=1, col=1) + fig.add_hline(y=30, line=dict(color="#3ddc97", dash="dot", width=1), row=1, col=1) + + hist_colors = np.where(ind["macd_hist"] >= 0, "#3ddc97", "#ff5c72") + fig.add_trace(go.Bar(x=df["ts"], y=ind["macd_hist"], name="Histogram", marker_color=hist_colors), row=2, col=1) + fig.add_trace(go.Scatter(x=df["ts"], y=ind["macd_line"], name="MACD", + line=dict(color="#f2b84b", width=1)), row=2, col=1) + fig.add_trace(go.Scatter(x=df["ts"], y=ind["macd_signal"], name="Sinyal", + line=dict(color="#5b8cff", width=1)), row=2, col=1) + + fig.add_trace(go.Scatter(x=df["ts"], y=ind["stoch_k"], name="%K", line=dict(color="#5b8cff")), row=3, col=1) + fig.add_trace(go.Scatter(x=df["ts"], y=ind["stoch_d"], name="%D", line=dict(color="#f2b84b")), row=3, col=1) + fig.add_hline(y=80, line=dict(color="#ff5c72", dash="dot", width=1), row=3, col=1) + fig.add_hline(y=20, line=dict(color="#3ddc97", dash="dot", width=1), row=3, col=1) + + fig.add_trace(go.Scatter(x=df["ts"], y=ind["adx"], name="ADX", line=dict(color="#e6ebf2", width=1.5)), row=4, col=1) + fig.add_trace(go.Scatter(x=df["ts"], y=ind["plus_di"], name="+DI", line=dict(color="#3ddc97", width=1)), row=4, col=1) + fig.add_trace(go.Scatter(x=df["ts"], y=ind["minus_di"], name="-DI", line=dict(color="#ff5c72", width=1)), row=4, col=1) + fig.add_hline(y=25, line=dict(color="#f2b84b", dash="dot", width=1), row=4, col=1) + + return _base_layout(fig, f"{symbol} · Momentum Göstergeleri", 720) + + +def build_equity_chart(df, symbol): + ret = df["c"].pct_change().fillna(0) + cum = (1 + ret).cumprod() + peak = cum.cummax() + dd = (cum - peak) / peak + fig = make_subplots(rows=2, cols=1, shared_xaxes=True, row_heights=[0.65, 0.35], vertical_spacing=0.06, + subplot_titles=("Kümülatif Getiri (Al ve Tut)", "Drawdown")) + fig.add_trace(go.Scatter(x=df["ts"], y=cum, name="Kümülatif Getiri", line=dict(color="#5b8cff"), + fill="tozeroy", fillcolor="rgba(91,140,255,0.08)"), row=1, col=1) + fig.add_trace(go.Scatter(x=df["ts"], y=dd, name="Drawdown", line=dict(color="#ff5c72"), + fill="tozeroy", fillcolor="rgba(255,92,114,0.12)"), row=2, col=1) + fig.update_layout(showlegend=False) + return _base_layout(fig, f"{symbol} · Risk Profili", 440) + + +def build_depth_chart(bids, asks, symbol): + bids_sorted = sorted(bids, key=lambda x: -x[0]) + asks_sorted = sorted(asks, key=lambda x: x[0]) + bid_prices = [p for p, _ in bids_sorted] + bid_cum = np.cumsum([s for _, s in bids_sorted]) + ask_prices = [p for p, _ in asks_sorted] + ask_cum = np.cumsum([s for _, s in asks_sorted]) + fig = go.Figure() + fig.add_trace(go.Scatter(x=bid_prices, y=bid_cum, name="Alış (Bid)", line=dict(color="#3ddc97", width=2), + fill="tozeroy", fillcolor="rgba(61,220,151,0.15)")) + fig.add_trace(go.Scatter(x=ask_prices, y=ask_cum, name="Satış (Ask)", line=dict(color="#ff5c72", width=2), + fill="tozeroy", fillcolor="rgba(255,92,114,0.15)")) + fig.update_layout(xaxis_title="Fiyat", yaxis_title="Kümülatif Miktar") + return _base_layout(fig, f"{symbol} · Emir Defteri Derinliği", 380) + + +# --------------------------------------------------------------------------- +# TAB CALLBACKS +# --------------------------------------------------------------------------- +def load_overview(symbol, bar): + try: + df = fetch_candles(symbol, bar, 300) + ticker = fetch_ticker(symbol) + ind = compute_indicators(df) + last = float(ticker.get("last") or df["c"].iloc[-1]) + open24h = float(ticker.get("open24h") or df["c"].iloc[0]) + high24h = float(ticker.get("high24h") or df["h"].max()) + low24h = float(ticker.get("low24h") or df["l"].min()) + vol24h = float(ticker.get("volCcy24h") or df["vol"].sum()) + chg = (last - open24h) / open24h if open24h else 0.0 + tone = "up" if chg >= 0 else "down" + cards = [ + metric_card("SON FİYAT", f"${fmt_price(last)}", "", tone), + metric_card("24S DEĞİŞİM", fmt_pct(chg), "", tone), + metric_card("24S YÜKSEK", f"${fmt_price(high24h)}"), + metric_card("24S DÜŞÜK", f"${fmt_price(low24h)}"), + metric_card("24S HACİM (USDT)", f"${fmt_large(vol24h)}"), + ] + return cards_row(cards), build_price_chart(df, ind, symbol, bar) + except Exception as e: + return error_box(f"Veri alınamadı: {e}"), go.Figure() + + +def _reasons_html(reasons): + icon = {"up": "▲", "down": "▼", "flat": "•"} + return "".join(f'
  • {icon[t]}{txt}
  • ' for t, txt in reasons) + + +def load_signal(symbol, bar, closed_only=False, acct=10000, risk_pct=1.0): + empty5 = lambda html: (html, go.Figure(), go.Figure(), go.Figure(), go.Figure()) + try: + df = fetch_candles(symbol, bar, 300) + if closed_only: + df = df[df["confirm"] == 1].reset_index(drop=True) + if len(df) < 30: + return empty5(error_box("Sinyal için yeterli mum verisi yok (en az 30 bar gerekir).")) + acct = float(acct or 0) + risk_pct = float(risk_pct or 0) + res = generate_signal(df, symbol=symbol, bar=bar, mtf=True, micro=True, closed_only=closed_only, + acct=acct, risk_pct=risk_pct) + ind, plan, bt = res["indicators"], res["plan"], res["backtest"] + + # ---- Ana panel ---- + regime_col = {"TREND": "#5b8cff", "YATAY": "#f2b84b", "GEÇİŞ": "#7c8797"}[res["regime"]] + panel = ( + f'
    ' + f'
    {res["signal"]}
    ' + f'
    KOMPOZİT SKOR
    ' + f'
    {res["score"]:+.2f} / ±10
    ' + f'
    GÜVEN
    ' + f'
    %{res["confidence"]}
    ' + f'
    KALİTE NOTU
    ' + f'
    {res["grade"]}
    ' + f'
    REJİM
    ' + f'
    {res["regime"]}
    ' + f'
    ' + ) + tot = res["bull_w"] + res["bear_w"] + bull_pct = (res["bull_w"] / tot * 100) if tot > 0 else 50.0 + consensus = ( + f'
    FAKTÖR KONSENSÜSÜ · BOĞA %{bull_pct:.0f} / AYI %{100 - bull_pct:.0f}
    ' + f'
    ' + f'
    ' + ) + conf_tags = "".join( + f'
    {k} %{v * 100:.0f}
    ' for k, v in res["conf_parts"].items()) + tag_html = "" + if res["tags"]: + tag_html = '
    ' + "".join( + f'
    {t}
    ' for t, c in res["tags"]) + "
    " + conf_html = f'
    {conf_tags}
    ' + + # ---- İşlem planı + pozisyon boyutu ---- + plan_cards, size_cards = [], [] + if plan: + up = plan["direction"] == "LONG" + plan_cards = [ + metric_card("ÖNERİLEN YÖN", plan["direction"], "", "up" if up else "down"), + metric_card("GİRİŞ", f"${fmt_price(plan['entry'])}"), + metric_card("STOP-LOSS", f"${fmt_price(plan['stop_loss'])}", + f"{plan['stop_type']} · %{plan['risk_frac'] * 100:.2f} risk", "down"), + ] + for k, (px, m) in enumerate(plan["targets"]): + plan_cards.append(metric_card(f"HEDEF {k + 1}", f"${fmt_price(px)}", f"{m:.2f}R", "up")) + sz = plan["sizing"] + if sz: + size_cards = [ + metric_card("RİSK TUTARI", f"${fmt_large(sz['risk_amount'])}", f"Hesabın %{risk_pct:g}'i"), + metric_card("POZİSYON (USDT)", f"${fmt_large(sz['notional'])}", f"Miktar: {sz['qty']:.6g}"), + metric_card("KALDIRAÇ ETKİSİ", f"{sz['leverage']:.2f}x", + "Spot ile yetmez — kaldıraç gerekir" if sz["leverage"] > 1.0 else "Spot ile uygulanabilir", + "down" if sz["leverage"] > 1.0 else "up"), + ] + stats = bt["stats"] if bt else None + if stats is not None: + k = stats["kelly"] + if stats["n"] < 20: + kv, ks, kt = "—", "Örnek az (<20 işlem)", "neutral" + elif k is None or k <= 0: + kv, ks, kt = "Kenar yok", "Kelly ≤ 0", "down" + else: + half = min(k / 2, 0.03) + kv, ks, kt = f"%{half * 100:.2f}", f"Yarım-Kelly (tavan %3) · bilgi amaçlı", "up" + size_cards.append(metric_card("ÖNERİLEN RİSK", kv, ks, kt)) + + # ---- Backtest kartları ---- + bt_cards = [] + if stats: + L, S = stats["long"], stats["short"] + wr = lambda x: f"%{x['win_rate'] * 100:.0f}" if x["win_rate"] is not None else "—" + bt_cards = [ + metric_card("İŞLEM SAYISI", f"{stats['n']}", f"Long {L['n']} ({wr(L)}) · Short {S['n']} ({wr(S)})"), + metric_card("KAZANMA ORANI", f"%{stats['win_rate'] * 100:.1f}", f"Maks. üst üste kayıp: {stats['max_consec_loss']}", + "up" if stats["win_rate"] >= 0.45 else "down"), + metric_card("PROFIT FACTOR", f"{stats['profit_factor']:.2f}", "Brüt kazanç / brüt kayıp", + "up" if stats["profit_factor"] >= 1.2 else "down"), + metric_card("BEKLENTİ / İŞLEM", fmt_pct(stats["expectancy"]), f"Ort. {stats['avg_r']:+.2f}R (komisyon dahil)", + "up" if stats["expectancy"] > 0 else "down"), + metric_card("SQN", f"{stats['sqn']:.2f}", "Sistem kalitesi (>2 iyi, >3 çok iyi)", + "up" if stats["sqn"] >= 2 else "neutral"), + metric_card("STRATEJİ MAKS. DD", fmt_pct(stats["max_dd"]), "", "down"), + metric_card("STRATEJİ vs AL&TUT", f"{fmt_pct(stats['total_return'])} / {fmt_pct(stats['bh_return'])}", + "Aynı dönem, komisyon dahil", "up" if stats["total_return"] > stats["bh_return"] else "down"), + ] + acc_cards = [] + if bt and bt["acc"]: + a = bt["acc"] + acc_cards.append(metric_card("YÖN DOĞRULUĞU", f"%{a['accuracy'] * 100:.1f}", + f"{a['n_signals']} sinyal · {a['forward_bars']} bar ileri", + "up" if a["accuracy"] >= 0.5 else "down")) + if a.get("trend_regime"): + t = a["trend_regime"] + acc_cards.append(metric_card("DOĞRULUK · TREND", f"%{t['accuracy'] * 100:.1f}", + f"{t['n_signals']} sinyal (ADX ≥ 25)", "up" if t["accuracy"] >= 0.5 else "down")) + if a.get("range_regime"): + t = a["range_regime"] + acc_cards.append(metric_card("DOĞRULUK · YATAY", f"%{t['accuracy'] * 100:.1f}", + f"{t['n_signals']} sinyal (ADX < 25)", "up" if t["accuracy"] >= 0.5 else "down")) + + # ---- Piyasa bağlamı ---- + ctx = [] + for m in res["mtf"]: + if m["aligned"] is None: + ctx.append(metric_card(f"{m['bar']} TEYİDİ", "Nötr", f"Üst zaman dilimi skoru: {m['score']}")) + else: + ctx.append(metric_card(f"{m['bar']} TEYİDİ", "Uyumlu ✓" if m["aligned"] else "Ters Yönlü ⚠", + f"Üst zaman dilimi skoru: {m['score']}", "up" if m["aligned"] else "down")) + if res["funding"] is not None: + f_ = res["funding"] + ctx.append(metric_card("FUNDING (8S)", f"{f_ * 100:+.4f}%", "Pozitif = long kalabalık", + "down" if f_ > 0.0002 else ("up" if f_ < -0.0002 else "neutral"))) + if res["imbalance"] is not None: + im = res["imbalance"] + ctx.append(metric_card("DEFTER DENGESİ", f"{im * 100:+.0f}%", "Derinlik ağırlıklı", + "up" if im > 0.2 else ("down" if im < -0.2 else "neutral"))) + if res["supports"]: + lv, tc = res["supports"][0] + ctx.append(metric_card("YAKIN DESTEK", f"${fmt_price(lv)}", f"{tc} temas · {(res['price'] - lv) / res['atr']:.1f} ATR", "up")) + if res["resists"]: + lv, tc = res["resists"][0] + ctx.append(metric_card("YAKIN DİRENÇ", f"${fmt_price(lv)}", f"{tc} temas · {(lv - res['price']) / res['atr']:.1f} ATR", "down")) + ctx.append(metric_card("VOLATİLİTE YÜZDELİĞİ", f"%{res['atr_rank'] * 100:.0f}", "ATR/fiyat, son 100 bar", + "down" if res["atr_rank"] > 0.9 else "neutral")) + + reasons_box = (f'
    FAKTÖR ANALİZİ
    ' + f'
    ') + bt_note = "" + if stats: + bt_note = ('
    Backtest: son ~220 barda sinyaller üst üste binmeden işleme çevrilir; giriş bir sonraki barın açılışı, ' + f'çıkış SL {SL_ATR}×ATR / TP {TP_ATR}×ATR / {MAX_HOLD_BARS} bar; komisyon %{FEE_PER_SIDE * 100:.1f}/yön dahildir. ' + 'Emir defteri, funding, S/R ve MTF katmanları canlıdır, geçmişe uygulanamaz.
    ') + + html = (panel + consensus + conf_html + tag_html + + (section("İŞLEM PLANI") + cards_row(plan_cards) if plan_cards else "") + + (section("POZİSYON BOYUTU") + cards_row(size_cards) if size_cards else "") + + (section("GEÇMİŞ PERFORMANS (WALK-FORWARD)") + cards_row(bt_cards) + cards_row(acc_cards) + bt_note + if (bt_cards or acc_cards) else "") + + section("PİYASA BAĞLAMI") + cards_row(ctx) + reasons_box) + return (html, build_signal_price_chart(df, ind, symbol, bar, res), build_factor_chart(res["contribs"], symbol), + build_backtest_chart(bt, df, symbol), build_signal_chart(df, ind, symbol)) + except Exception as e: + return empty5(error_box(f"Veri alınamadı: {e}")) + + +def load_risk(symbol, bar): + try: + df = fetch_candles(symbol, bar, 300) + rm = risk_metrics(df, bar) + if rm is None: + return error_box("Risk hesaplaması için yeterli veri yok."), go.Figure() + cards = [ + metric_card("YILLIKLANDIRILMIŞ VOLATİLİTE", fmt_pct(rm["volatility_annual"])), + metric_card("YILLIKLANDIRILMIŞ GETİRİ", fmt_pct(rm["return_annual"]), "", + "up" if rm["return_annual"] >= 0 else "down"), + metric_card("SHARPE ORANI", f"{rm['sharpe']:.2f}", "", "up" if rm["sharpe"] > 0 else "down"), + metric_card("MAKS. DRAWDOWN", fmt_pct(rm["max_drawdown"]), "", "down"), + metric_card("GÜNCEL DRAWDOWN", fmt_pct(rm["current_dd"]), "", + "down" if rm["current_dd"] < 0 else "neutral"), + metric_card("VaR %95 (BAR)", fmt_pct(rm["var95_bar"]), "", "down"), + metric_card("CVaR %95 (BAR)", fmt_pct(rm["cvar95_bar"]), "", "down"), + ] + return cards_row(cards), build_equity_chart(df, symbol) + except Exception as e: + return error_box(f"Veri alınamadı: {e}"), go.Figure() + + +def load_orderbook(symbol, depth): + try: + ob = fetch_orderbook(symbol, int(depth)) + bids = [(float(p), float(s)) for p, s, *_ in ob.get("bids", [])] + asks = [(float(p), float(s)) for p, s, *_ in ob.get("asks", [])] + if not bids or not asks: + return error_box("Emir defteri verisi alınamadı."), go.Figure() + best_bid, best_ask = bids[0][0], asks[0][0] + spread = best_ask - best_bid + spread_pct = spread / best_ask if best_ask else 0.0 + bid_vol = sum(s for _, s in bids) + ask_vol = sum(s for _, s in asks) + imbalance = (bid_vol - ask_vol) / (bid_vol + ask_vol) if (bid_vol + ask_vol) else 0.0 + tone = "up" if imbalance > 0.05 else ("down" if imbalance < -0.05 else "neutral") + cards = [ + metric_card("EN İYİ ALIŞ", f"${fmt_price(best_bid)}", "", "up"), + metric_card("EN İYİ SATIŞ", f"${fmt_price(best_ask)}", "", "down"), + metric_card("SPREAD", f"${fmt_price(spread)}", fmt_pct(spread_pct)), + metric_card("DEFTER DENGESİZLİĞİ", fmt_pct(imbalance), "Pozitif = alış baskısı", tone), + ] + return cards_row(cards), build_depth_chart(bids, asks, symbol) + except Exception as e: + return error_box(f"Veri alınamadı: {e}"), go.Figure() + + +@_gpu +def refresh_all(symbol, bar, depth, closed_only, acct, risk_pct): + ov_html, ov_fig = load_overview(symbol, bar) + sig = load_signal(symbol, bar, closed_only, acct, risk_pct) + risk_html, risk_fig = load_risk(symbol, bar) + ob_html, ob_fig = load_orderbook(symbol, depth) + return (ov_html, ov_fig, *sig, risk_html, risk_fig, ob_html, ob_fig) + + +def refresh_signal(symbol, bar, closed_only, acct, risk_pct): + """Yalnızca sinyal sekmesini yeniler (risk/kapanmış-mum ayarı değişince hızlı güncelleme).""" + return load_signal(symbol, bar, closed_only, acct, risk_pct) + + +SCAN_MODES = ["Tümü (skora göre)", "Yalnızca AL yönlü", "Yalnızca SAT yönlü", "Yalnızca yüksek kalite (A / A+)"] + + +def _scan_one(sym, bar, all_tickers, closed_only): + df = fetch_candles(sym, bar, 300) + if closed_only: + df = df[df["confirm"] == 1].reset_index(drop=True) + # MTF ve mikro-yapı katmanları taramada kapalı (API yükünü azaltmak için); tekil analizde açık. + res = generate_signal(df, symbol=sym, bar=bar, mtf=False, micro=False, closed_only=closed_only) + rm = risk_metrics(df, bar) + t = all_tickers.get(sym, {}) + last = float(t.get("last") or df["c"].iloc[-1]) + open24h = float(t.get("open24h") or df["c"].iloc[0]) + chg24h = (last - open24h) / open24h if open24h else 0.0 + div = res["divergence"] + div_txt = "Yükseliş" if div["bullish"] else ("Düşüş" if div["bearish"] else "—") + bt = res["backtest"] + st = bt["stats"] if bt else None + acc = bt["acc"] if bt else None + return { + "Sembol": sym, "Fiyat": last, "24S %": round(chg24h * 100, 2), "Sinyal": res["signal"], + "Skor": res["score"], "Kalite": res["grade"], "Güven %": res["confidence"], "Rejim": res["regime"], + "RSI": round(res["indicators"]["rsi"].iloc[-1], 1), "ADX": round(res["adx"], 1), + "Sıkışma": "●" if res["squeeze_on"] else "—", "Uyumsuzluk": div_txt, + "Kazanma %": round(st["win_rate"] * 100, 1) if st else None, + "Profit Factor": round(st["profit_factor"], 2) if st else None, + "Beklenti %": round(st["expectancy"] * 100, 2) if st else None, + "Yön Doğruluğu %": round(acc["accuracy"] * 100, 1) if acc else None, + "Volatilite (Yıllık) %": round(rm["volatility_annual"] * 100, 1) if rm else None, + "Sharpe": round(rm["sharpe"], 2) if rm else None, + } + + +def run_scanner(symbols_text, bar, closed_only=False, mode=SCAN_MODES[0], progress=gr.Progress()): + symbols = [s.strip().upper() for s in symbols_text.split(",") if s.strip()][:25] + if not symbols: + return pd.DataFrame() + try: + all_tickers = {t["instId"]: t for t in fetch_all_tickers("SPOT")} + except Exception: + all_tickers = {} + + rows, done = [], 0 + with ThreadPoolExecutor(max_workers=4) as ex: # ağ beklemesini paralelle (OKX limiti için 4 ile sınırlı) + futs = {ex.submit(_scan_one, s, bar, all_tickers, closed_only): s for s in symbols} + for f in as_completed(futs): + done += 1 + progress(done / len(symbols), desc=f"{futs[f]} tarandı ({done}/{len(symbols)})") + try: + rows.append(f.result()) + except Exception: + continue + if not rows: + return pd.DataFrame() + out = pd.DataFrame(rows) + if mode == SCAN_MODES[1]: + out = out[out["Skor"] >= SIGNAL_THRESHOLD].sort_values("Skor", ascending=False) + elif mode == SCAN_MODES[2]: + out = out[out["Skor"] <= -SIGNAL_THRESHOLD].sort_values("Skor", ascending=True) + elif mode == SCAN_MODES[3]: + out = out[out["Kalite"].isin(["A", "A+"])].sort_values("Güven %", ascending=False) + else: + out = out.sort_values("Skor", ascending=False) + return out.reset_index(drop=True) + + +# --------------------------------------------------------------------------- +# STYLE — dark "trading terminal" identity (deliberate, not default SaaS chrome) +# --------------------------------------------------------------------------- +HEAD = """ + + + +""" + +CSS = """ +:root { + --bg-void: #0a0e14; + --bg-panel: #12171f; + --line: #232a35; + --text-primary: #e6ebf2; + --text-muted: #7c8797; + --accent-long: #3ddc97; + --accent-short: #ff5c72; + --accent-signal: #f2b84b; + --accent-primary: #5b8cff; +} +.gradio-container { background: var(--bg-void) !important; color: var(--text-primary) !important; + font-family: 'IBM Plex Sans', system-ui, sans-serif !important; } +h1, h2, h3 { font-family: 'Space Grotesk', sans-serif !important; letter-spacing: -0.01em; } + +#masthead { display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:10px; + border-bottom: 1px solid var(--line); padding: 14px 4px 18px 4px; margin-bottom: 6px; } +#masthead .wordmark { font-family:'Space Grotesk',sans-serif; font-weight:700; font-size:1.5rem; letter-spacing:-0.02em; } +#masthead .wordmark span { color: var(--accent-primary); } +#masthead .tags { display:flex; gap:8px; flex-wrap:wrap; } +#masthead .tag { font-size:0.7rem; padding:3px 9px; border:1px solid var(--line); border-radius:3px; + color:var(--text-muted); font-family:'IBM Plex Mono',monospace; } + +.metric-row { display:flex; flex-wrap:wrap; gap:10px; margin:8px 0 16px 0; } +.metric-card { background: var(--bg-panel); border:1px solid var(--line); border-radius:4px; + padding:12px 16px; min-width:150px; flex:1; } +.mc-label { font-size:0.66rem; color:var(--text-muted); letter-spacing:0.04em; margin-bottom:6px; } +.mc-value { font-size:1.24rem; font-weight:600; color:var(--text-primary); + font-family:'IBM Plex Mono',monospace; } +.mc-sub { font-size:0.72rem; color:var(--text-muted); margin-top:4px; font-family:'IBM Plex Mono',monospace; } +.metric-card.mc-up .mc-value { color: var(--accent-long); } +.metric-card.mc-down .mc-value { color: var(--accent-short); } + +.section-title { font-family:'Space Grotesk',sans-serif; font-size:0.78rem; font-weight:600; letter-spacing:0.08em; + color:var(--text-muted); margin:18px 0 0 0; padding-top:12px; border-top:1px solid var(--line); } + +.signal-panel { display:flex; align-items:center; flex-wrap:wrap; gap:20px; background:var(--bg-panel); + border:1px solid var(--line); border-radius:4px; padding:18px 20px; margin-bottom:12px; } +.signal-badge { font-family:'Space Grotesk',sans-serif; font-weight:700; font-size:1.6rem; color:var(--tone); + border:2px solid var(--tone); border-radius:4px; padding:10px 22px; } +.signal-meta .mc-value { font-size:1.35rem; } + +.consensus { background:var(--bg-panel); border:1px solid var(--line); border-radius:4px; padding:12px 16px; margin-bottom:12px; } +.cbar { display:flex; height:8px; border-radius:2px; overflow:hidden; background:var(--line); } +.cbull { background: var(--accent-long); } +.cbear { background: var(--accent-short); } + +.tags { display:flex; gap:8px; flex-wrap:wrap; } +.tags .tag { font-size:0.72rem; font-weight:600; padding:5px 12px; border:1px solid var(--line); + border-radius:3px; font-family:'IBM Plex Mono',monospace; letter-spacing:0.03em; } + +.reasons-box { background:var(--bg-panel); border:1px solid var(--line); border-radius:4px; padding:16px 20px; margin-top:8px; } +.reasons-box ul { margin:8px 0 0 0; padding-left:0; list-style:none; } +.reasons-box li { margin-bottom:6px; color:var(--text-primary); font-size:0.9rem; } +.reasons-box .ri { display:inline-block; width:18px; font-size:0.7rem; } +.reasons-box .r-up .ri { color: var(--accent-long); } +.reasons-box .r-down .ri { color: var(--accent-short); } +.reasons-box .r-flat .ri { color: var(--text-muted); } + +.note { color:var(--text-muted); font-size:0.74rem; line-height:1.5; margin:-6px 0 8px 0; } + +.error-box { background: rgba(255,92,114,0.08); border:1px solid var(--accent-short); color:var(--accent-short); + padding:14px 18px; border-radius:4px; font-family:'IBM Plex Mono',monospace; font-size:0.85rem; } + +.disclaimer { border-top:1px solid var(--line); margin-top:22px; padding-top:14px; color:var(--text-muted); + font-size:0.76rem; line-height:1.55; } +""" + +# --------------------------------------------------------------------------- +# GRADIO UI +# --------------------------------------------------------------------------- +with gr.Blocks(title="OKX Quant Terminal") as demo: + + gr.HTML( + '
    ' + '
    OKX QUANT TERMINAL
    ' + '
    ELITE SIGNAL ENGINE v2
    ' + '
    PUBLIC MARKET DATA
    ' + '
    READ-ONLY · EMİR GÖNDERİLMEZ
    ' + '
    ' + ) + + with gr.Row(): + symbol_dd = gr.Dropdown(choices=POPULAR_SYMBOLS, value="BTC-USDT", allow_custom_value=True, + label="Sembol", scale=2) + bar_dd = gr.Dropdown(choices=BAR_OPTIONS, value="1H", label="Zaman Dilimi", scale=1) + depth_slider = gr.Slider(5, 50, value=20, step=5, label="Defter Derinliği", scale=1) + refresh_btn = gr.Button("🔄 Yenile", variant="primary", scale=1) + auto_cb = gr.Checkbox(label="Otomatik (15sn)", value=False, scale=1) + + with gr.Row(): + closed_cb = gr.Checkbox(label="Sadece kapanmış mumlarla sinyal (repaint yok)", value=False, scale=2) + acct_num = gr.Number(value=10000, label="Hesap Büyüklüğü (USDT)", minimum=0, scale=1) + risk_sl = gr.Slider(0.25, 5, value=1, step=0.25, label="İşlem Başına Risk (%)", scale=1) + + timer = gr.Timer(15, active=False) + + with gr.Tabs(): + with gr.Tab("📊 Genel Bakış"): + ov_html = gr.HTML() + ov_plot = gr.Plot() + with gr.Tab("🎯 Sinyal Motoru"): + sig_html = gr.HTML() + sig_map_plot = gr.Plot() + sig_factor_plot = gr.Plot() + sig_bt_plot = gr.Plot() + sig_ind_plot = gr.Plot() + with gr.Tab("⚠️ Risk Analitiği"): + risk_html = gr.HTML() + risk_plot = gr.Plot() + with gr.Tab("📖 Emir Defteri"): + ob_html = gr.HTML() + ob_plot = gr.Plot() + with gr.Tab("🔍 Çoklu Varlık Tarayıcı"): + gr.Markdown("Popüler pariteleri kompozit sinyal skoruna göre sıralar. Kendi listenizi virgülle ayırıp girebilirsiniz (en fazla 25 sembol).") + with gr.Row(): + scan_symbols = gr.Textbox(value=",".join(POPULAR_SYMBOLS), label="Semboller", scale=3) + scan_bar = gr.Dropdown(choices=BAR_OPTIONS, value="4H", label="Zaman Dilimi", scale=1) + scan_mode = gr.Dropdown(choices=SCAN_MODES, value=SCAN_MODES[0], label="Filtre", scale=2) + scan_btn = gr.Button("🔍 Taramayı Başlat", variant="primary") + scan_out = gr.Dataframe(interactive=False, wrap=True) + + gr.HTML( + '
    Bu panel yalnızca eğitim ve bilgilendirme amaçlıdır; yatırım tavsiyesi ' + 'değildir. Tüm veriler OKX genel (public) piyasa API\'sinden alınır; hiçbir API anahtarı ' + 'kullanılmaz ve hiçbir gerçek emir gönderilmez. Kripto varlıklar yüksek oynaklık içerir; geçmiş ' + 'performans (backtest dahil) gelecekteki sonuçların göstergesi değildir. Backtest sonuçları ' + 'kısa örneklem üzerindedir ve istatistiksel olarak sınırlıdır.
    ' + ) + + io_inputs = [symbol_dd, bar_dd, depth_slider, closed_cb, acct_num, risk_sl] + io_outputs = [ov_html, ov_plot, sig_html, sig_map_plot, sig_factor_plot, sig_bt_plot, sig_ind_plot, + risk_html, risk_plot, ob_html, ob_plot] + sig_inputs = [symbol_dd, bar_dd, closed_cb, acct_num, risk_sl] + sig_outputs = [sig_html, sig_map_plot, sig_factor_plot, sig_bt_plot, sig_ind_plot] + + refresh_btn.click(refresh_all, io_inputs, io_outputs) + timer.tick(refresh_all, io_inputs, io_outputs) + auto_cb.change(lambda active: gr.Timer(active=active), auto_cb, timer) + closed_cb.change(refresh_signal, sig_inputs, sig_outputs) + acct_num.submit(refresh_signal, sig_inputs, sig_outputs) + risk_sl.release(refresh_signal, sig_inputs, sig_outputs) + scan_btn.click(run_scanner, [scan_symbols, scan_bar, closed_cb, scan_mode], scan_out) + demo.load(refresh_all, io_inputs, io_outputs) + +demo.queue().launch( + theme=gr.themes.Base(primary_hue="blue", neutral_hue="slate"), + css=CSS, + head=HEAD, + ssr_mode=False, +)