diff --git "a/app.py" "b/app.py" new file mode 100644--- /dev/null +++ "b/app.py" @@ -0,0 +1,592 @@ +""" +OKX QUANT TERMINAL · v3 — SMART MONEY CONCEPTS (SMC) SİNYAL MOTORU +Kripto piyasa analitiği paneli (Gradio). + +Yalnızca OKX'in herkese açık (anahtarsız) piyasa verisi REST API'si kullanılır: + - /api/v5/market/candles, /ticker, /tickers, /books + - /api/v5/public/funding-rate (türev duyarlılığı, isteğe bağlı bilgi) + +API anahtarı yok, emir gönderimi yok, hesap erişimi yok. Salt-okunur analiz / sinyal terminalidir. + +v3 — AĞIRLIKSIZ SMC SİNYAL MOTORU + * Eski ağırlıklı kompozit skor (trend/reversion/Fib/ısı haritası/RSI uyumsuzluğu ağırlıkları, ADX rejim + geçişi, tanh sıkıştırma, skor ivmesi, MTF/emir defteri/funding puan ekleri) TAMAMEN KALDIRILDI. + * YÖN = piyasa yapısı: son onaylı swing'in kapanışla kırılması (BOS = devam, CHoCH = dönüş). + Yapı yükselişse yalnızca long, düşüşse yalnızca short aranır; yapıya ters teyit varsa sinyal yok. + * TEYİTLER (her biri eşit, ±1): taze BOS/CHoCH · premium/discount · order block · FVG retest · likidite avı · Acheron VWAP etkileşimi. + Skor = net teyit sayısı (±6). AL/SAT: |skor| ≥ 2, GÜÇLÜ: |skor| ≥ 4 + teyit uyumu + üst zaman dilimi yapısı uyumu. + * Üst zaman dilimi (MTF), emir defteri, funding, S/R: yalnızca BİLGİ / filtre — skora puan eklemez. + * LLM katmanı kaldırıldı. Fiyat grafikleri 3D (Plotly Scatter3d / Mesh3d / Surface). + * Güven skoru bileşenlerin eşit ağırlıklı ortalamasıdır. Stop: order block / likidite avı ekstremi arkası. + * Gerçek işlem simülasyonu (SL/TP/zaman çıkışı + komisyon): kazanma oranı, profit factor, beklenti, R, SQN, + drawdown, Al&Tut karşılaştırması, yarım-Kelly risk önerisi. Canlı sinyal ve backtest AYNI nedensel fonksiyonu kullanır. + * RSI / MACD / Stokastik / ADX grafikleri bilgi amaçlıdır; sinyale dahil değildir. +""" +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", +} + +# --- Sinyal motoru parametreleri --- +SIGNAL_THRESHOLD = 2.0 # |net SMC teyit sayısı| >= bu değer -> AL / SAT (her teyit eşit: ±1) +STRONG_THRESHOLD = 4.0 # |net teyit| >= bu değer + konsensüs + MTF uyumu -> GÜÇLÜ +WARMUP_BARS = 60 # yapı (swing/BOS) oluşması 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ışı +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) +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) + +# --- Smart Money Concepts (SMC) parametreleri --- +SMC_SWING = 3 # swing pivotu için her iki yanda gereken bar (pivot w bar sonra onaylanır) +SMC_FRESH_BARS = 8 # BOS/CHoCH bu kadar bar boyunca 'taze' sayılır +SMC_SWEEP_BARS = 5 # likidite avı bu kadar bar boyunca geçerli +SMC_OB_LOOKBACK = 40 # order block orijini için geriye bakış (bar) +SMC_OB_MAX_AGE = 120 # order block / FVG en fazla bu kadar bar yaşar +SMC_FVG_MIN_ATR = 0.15 # FVG boşluğu en az bu kadar ATR olmalı (gürültü filtresi) +SMC_FVG_KEEP = 6 # takip edilen açık FVG sayısı (yön başına) +VWAP_FRESH_BARS = 4 # VWAP kesişimi bu kadar bar boyunca 'taze' sayılır +VWAP_TOUCH_ATR = 0.25 # VWAP retest: fitil VWAP'ın bu kadar ATR yakınına değmeli +VWAP_ROLL_BARS = 48 # günlük ve üstü mumlarda oturum yok -> kayan pencere VWAP +VWAP_BAND_K = 1.0 # VWAP bantları: hacim ağırlıklı standart sapma çarpanı +SMC_PD_BAND = 0.05 # equilibrium (%50) etrafındaki nötr band + + +# --------------------------------------------------------------------------- +# 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 vwap(df, band_k=VWAP_BAND_K): + """Nedensel VWAP + hacim ağırlıklı sapma bantları. + Gün içi mumlar: her UTC gününde sıfırlanan oturum VWAP'ı. Günlük ve üstü: kayan VWAP_ROLL_BARS penceresi. + Tipik fiyat = (H+L+C)/3. Bar i'nin değeri yalnızca <= i verisini kullanır (ileri sızıntı yok).""" + tp = (df["h"] + df["l"] + df["c"]) / 3.0 + v = df["vol"].astype(float).clip(lower=0) + step = df["ts"].diff().median() + if pd.notna(step) and step < pd.Timedelta(days=1): + g = df["ts"].dt.floor("D") + cv = v.groupby(g).cumsum() + cpv = (tp * v).groupby(g).cumsum() + cpv2 = (tp * tp * v).groupby(g).cumsum() + else: + cv = v.rolling(VWAP_ROLL_BARS, min_periods=1).sum() + cpv = (tp * v).rolling(VWAP_ROLL_BARS, min_periods=1).sum() + cpv2 = (tp * tp * v).rolling(VWAP_ROLL_BARS, min_periods=1).sum() + cv = cv.replace(0, np.nan) + vw = cpv / cv + var = (cpv2 / cv - vw * vw).clip(lower=0) + sd = np.sqrt(var) + return vw, vw + band_k * sd, vw - band_k * sd + + +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 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 _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 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 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 smc_structure(df, atr_s, w=SMC_SWING): + """Smart Money Concepts yapısı — tamamen NEDENSEL (bar i yalnızca i'ye kadar olan veriyi kullanır; + swing pivotları `w` bar sonra onaylanır). Üretir: + * Piyasa yapısı: son onaylı swing high/low'un KAPANIŞLA kırılması. Trend yönünde kırılım = BOS (devam), + trende karşı kırılım = CHoCH (dönüş). + * Order Block: kırılımı başlatan hareketin orijinindeki son karşı yönlü mum (kapanışla aşılırsa geçersiz). + * Fair Value Gap: 3 mumluk fiyat boşluğu (kapanışla tamamen dolarsa geçersiz) + retest takibi. + * Likidite avı (sweep): fitil swing seviyesini aşar, kapanış seviyenin gerisinde kalır. + Dönüş: (seri sözlüğü, grafik için bölgeler)""" + o, h, l, c = (df[k].to_numpy(dtype=float) for k in ("o", "h", "l", "c")) + a = atr_s.to_numpy(dtype=float) + n = len(c) + full = lambda v: np.full(n, v, dtype=float) + out = {k: full(np.nan) for k in ("sh", "sl", "obb_lo", "obb_hi", "obs_lo", "obs_hi", "swb_px", "sws_px")} + out.update({k: full(0.0) for k in ("trend", "brk_dir", "brk_choch", "fvgb", "fvgs")}) + out.update({k: full(-1.0) for k in ("brk_i", "swb_i", "sws_i")}) + + last_sh = last_sl = None # (indeks, fiyat) — son onaylı swing high / low + sh_broken = sl_broken = True + sh_swept = sl_swept = False + trend = 0 + brk_dir, brk_i, brk_choch = 0, -1, 0 + bull_ob = bear_ob = None # (alt, üst, oluşum_bar, mum_bar) + bull_fvg, bear_fvg = [], [] # [(alt, üst, oluşum_bar), ...] + swb_i = sws_i = -1 + swb_px = sws_px = np.nan + events, sweeps = [], [] + + for i in range(n): + j = i - w + if j >= w: + if h[j] == h[j - w:j + w + 1].max(): + last_sh, sh_broken, sh_swept = (j, h[j]), False, False + if l[j] == l[j - w:j + w + 1].min(): + last_sl, sl_broken, sl_swept = (j, l[j]), False, False + ci = c[i] + + # --- yapı kırılımı (kapanışla): BOS / CHoCH + order block --- + if last_sh and not sh_broken and ci > last_sh[1]: + choch = 1 if trend < 0 else 0 + events.append({"i": i, "pivot_i": last_sh[0], "dir": 1, "choch": bool(choch), "level": float(last_sh[1])}) + trend, sh_broken = 1, True + brk_dir, brk_i, brk_choch = 1, i, choch + a0 = max(last_sl[0] if last_sl else 0, i - SMC_OB_LOOKBACK) + cand = [k for k in range(a0, i) if c[k] < o[k]] # düşüş mumları + if cand: + k = min(cand, key=lambda x: l[x]) # hareketin orijini: en düşük düşüş mumu + bull_ob = (float(l[k]), float(h[k]), i, k) + elif last_sl and not sl_broken and ci < last_sl[1]: + choch = 1 if trend > 0 else 0 + events.append({"i": i, "pivot_i": last_sl[0], "dir": -1, "choch": bool(choch), "level": float(last_sl[1])}) + trend, sl_broken = -1, True + brk_dir, brk_i, brk_choch = -1, i, choch + a0 = max(last_sh[0] if last_sh else 0, i - SMC_OB_LOOKBACK) + cand = [k for k in range(a0, i) if c[k] > o[k]] # yükseliş mumları + if cand: + k = max(cand, key=lambda x: h[x]) + bear_ob = (float(l[k]), float(h[k]), i, k) + + # --- likidite avı (sweep): fitil seviyeyi aşar, kapanış geri döner --- + if last_sh and not sh_broken and not sh_swept and h[i] > last_sh[1] and ci <= last_sh[1]: + sh_swept, sws_i, sws_px = True, i, float(h[i]) + sweeps.append({"i": i, "dir": -1, "level": float(last_sh[1])}) # buy-side likidite alındı + if last_sl and not sl_broken and not sl_swept and l[i] < last_sl[1] and ci >= last_sl[1]: + sl_swept, swb_i, swb_px = True, i, float(l[i]) + sweeps.append({"i": i, "dir": 1, "level": float(last_sl[1])}) # sell-side likidite alındı + + # --- order block mitigasyonu (kapanışla aşıldıysa ya da çok eskiyse geçersiz) --- + if bull_ob and bull_ob[2] < i and (ci < bull_ob[0] or i - bull_ob[2] > SMC_OB_MAX_AGE): + bull_ob = None + if bear_ob and bear_ob[2] < i and (ci > bear_ob[1] or i - bear_ob[2] > SMC_OB_MAX_AGE): + bear_ob = None + + # --- Fair Value Gap: oluşum, mitigasyon, retest --- + if i >= 2: + min_gap = SMC_FVG_MIN_ATR * a[i] if a[i] == a[i] else 0.0 + if l[i] > h[i - 2] and l[i] - h[i - 2] >= min_gap: + bull_fvg.append((float(h[i - 2]), float(l[i]), i)) + if h[i] < l[i - 2] and l[i - 2] - h[i] >= min_gap: + bear_fvg.append((float(h[i]), float(l[i - 2]), i)) + bull_fvg = [z for z in bull_fvg if not (z[2] < i and ci < z[0]) and i - z[2] <= SMC_OB_MAX_AGE][-SMC_FVG_KEEP:] + bear_fvg = [z for z in bear_fvg if not (z[2] < i and ci > z[1]) and i - z[2] <= SMC_OB_MAX_AGE][-SMC_FVG_KEEP:] + out["fvgb"][i] = 1.0 if any(z[2] <= i - 2 and l[i] <= z[1] and ci >= z[0] for z in bull_fvg) else 0.0 + out["fvgs"][i] = 1.0 if any(z[2] <= i - 2 and h[i] >= z[0] and ci <= z[1] for z in bear_fvg) else 0.0 + + out["trend"][i] = trend + out["brk_dir"][i], out["brk_i"][i], out["brk_choch"][i] = brk_dir, brk_i, brk_choch + if last_sh: + out["sh"][i] = last_sh[1] + if last_sl: + out["sl"][i] = last_sl[1] + if bull_ob: + out["obb_lo"][i], out["obb_hi"][i] = bull_ob[0], bull_ob[1] + if bear_ob: + out["obs_lo"][i], out["obs_hi"][i] = bear_ob[0], bear_ob[1] + out["swb_i"][i], out["sws_i"][i], out["swb_px"][i], out["sws_px"][i] = swb_i, sws_i, swb_px, sws_px + + zones = {"bull_ob": bull_ob, "bear_ob": bear_ob, "bull_fvg": bull_fvg, "bear_fvg": bear_fvg, + "events": events, "sweeps": sweeps} + return {k: pd.Series(v, index=df.index) for k, v in out.items()}, zones + + +def compute_indicators(df): + close = df["c"] + ind = {} + # --- bilgi amaçlı göstergeler (SİNYALE DAHİL DEĞİL) --- + 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["vwap"], ind["vwap_up"], ind["vwap_dn"] = vwap(df) + ind["stoch_k"], ind["stoch_d"] = stochastic(df) + ind["adx"], ind["plus_di"], ind["minus_di"] = adx(df) + ind["vp_poc"], ind["vp_val"], ind["vp_vah"], ind["vp_heat"] = volume_heat(df) + ind["rvol"] = (df["vol"] / sma(df["vol"], 20).replace(0, np.nan)).fillna(1.0) + ind["atr_rank"] = (ind["atr"] / close).rolling(100, min_periods=30).apply( + lambda x: float((x <= x[-1]).mean()), raw=True).fillna(0.5) + # --- Smart Money Concepts (sinyalin tek kaynağı) --- + smc, zones = smc_structure(df, ind["atr"]) + for k, v in smc.items(): + ind["smc_" + k] = v + # 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 + ind["smc_zones"] = zones + return ind + + +# --------------------------------------------------------------------------- +# COMPOSITE SIGNAL ENGINE v2 (multi-factor, rejim-uyarlamalı, konsensüs filtreli) +# --------------------------------------------------------------------------- +def _score_at(ind, i, verbose=False): + """Smart Money Concepts teyit sayımı — AĞIRLIK YOK. Verilen bar indeksinde yalnızca o ana kadarki veri kullanılır. + + YÖN FİLTRESİ (kural, ağırlık değil): piyasa yapısı. Son kırılım yukarıysa (BOS/CHoCH ▲) yalnızca LONG, + aşağıysa yalnızca SHORT aranır. Yapıya ters teyitler varsa sinyal verilmez — önce CHoCH beklenir. + + TEYİTLER (her biri eşit: +1 boğa / −1 ayı / 0 yok): + 1) Taze BOS / CHoCH (son SMC_FRESH_BARS bar içinde yapı kırılımı) + 2) Premium / Discount (swing aralığının alt %45'i = discount → +1, üst %45'i = premium → −1) + 3) Order Block (fiyat aktif talep / arz bloğuna değdi) + 4) Fair Value Gap retest (fiyat açık FVG'ye geri döndü) + 5) Likidite avı (sweep) (son SMC_SWEEP_BARS bar içinde swing fitille süpürülüp geri dönüldü) + 6) Acheron VWAP etkileşimi (VWAP'ı taze yukarı/aşağı kesme veya VWAP'a retest + kapanışla tutunma) + + Skor = net teyit sayısı (−6…+6), yapı filtresini geçerse. Canlı sinyal ve backtest AYNI fonksiyonu kullanır.""" + A = ind["_arr"] + price, hi_i, lo_i = A["c"][i], A["h"][i], A["l"][i] + trend = int(A["smc_trend"][i]) + votes, tags = [], [] + long_refs, short_refs = [], [] # stop yerleşimi için SMC referans seviyeleri + + def vote(label, d, text, tag=None): + votes.append((label, int(d), text)) + if tag: + tags.append((tag, int(d))) + + # --- 1) taze BOS / CHoCH --- + bi = A["smc_brk_i"][i] + if bi >= 0 and i - bi <= SMC_FRESH_BARS: + d = int(A["smc_brk_dir"][i]) + name = "CHoCH" if A["smc_brk_choch"][i] > 0 else "BOS" + arrow = "▲" if d > 0 else "▼" + age = int(i - bi) + when = "bu barda" if age == 0 else f"{age} bar önce" + kind = "trend dönüşü" if name == "CHoCH" else "trend devamı" + vote(name, d, f"{name} {arrow}: piyasa yapısı {when} {'yukarı' if d > 0 else 'aşağı'} kırıldı ({kind})", + f"{name} {arrow}") + + # --- 2) Premium / Discount --- + sh, sl = A["smc_sh"][i], A["smc_sl"][i] + pd_pos = None + if sh == sh and sl == sl and sh > sl: + pd_pos = float((price - sl) / (sh - sl)) + if pd_pos <= 0.5 - SMC_PD_BAND: + vote("Discount", 1, f"Fiyat swing aralığının DISCOUNT bölgesinde (%{pd_pos * 100:.0f}) — alım için ucuz bölge", "DISCOUNT") + elif pd_pos >= 0.5 + SMC_PD_BAND: + vote("Premium", -1, f"Fiyat swing aralığının PREMIUM bölgesinde (%{pd_pos * 100:.0f}) — satım için pahalı bölge", "PREMIUM") + else: + vote("Denge", 0 \ No newline at end of file