diff --git "a/app.py" "b/app.py" deleted file mode 100644--- "a/app.py" +++ /dev/null @@ -1,1961 +0,0 @@ -""" -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/LLM 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ı. - Skor = net teyit sayısı (±5). 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, LLM: yalnızca BİLGİ / filtre — skora puan eklemez. - * 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 json -import math -import os -import re -import sys -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) -# Herkese açık (anahtarsız) OpenAI-uyumlu sohbet uç noktaları — SIRAYLA denenir, ilk geçerli yanıt kullanılır. -# model=None ise uç noktanın GET /models listesinden ilk model otomatik seçilir. -# Kendi servisinizi en başa eklemek için: LLM_BASE_URL (+ LLM_MODEL, LLM_API_KEY) ortam değişkenleri. -LLM_DEFAULT_ENDPOINTS = [ - {"url": "https://text.pollinations.ai/openai", "model": "openai", "key": ""}, - {"url": "https://api.llm7.io/v1", "model": None, "key": "unused"}, -] -LLM_BASE_URL = os.environ.get("LLM_BASE_URL", "").strip() -LLM_MODEL = os.environ.get("LLM_MODEL", "").strip() or None -LLM_API_KEY = os.environ.get("LLM_API_KEY", "").strip() # isteğe bağlı -LLM_TIMEOUT = 25 # saniye -LLM_CACHE_TTL = 180 # aynı mum + aynı skor için tekrar çağırma (maliyet/gecikme) -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) -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 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["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ü) - - Skor = net teyit sayısı (−5…+5), 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, f"Fiyat equilibrium (%50) civarında (%{pd_pos * 100:.0f}) — bölge avantajı yok") - - # --- 3) Order Block --- - bl, bh = A["smc_obb_lo"][i], A["smc_obb_hi"][i] - if bl == bl and lo_i <= bh and price >= bl: - vote("Order Block ▲", 1, f"Fiyat aktif talep (bullish) ORDER BLOCK bölgesinde ({fmt_price(bl)} – {fmt_price(bh)})", "OB ▲") - long_refs.append(float(bl)) - rl, rh = A["smc_obs_lo"][i], A["smc_obs_hi"][i] - if rl == rl and hi_i >= rl and price <= rh: - vote("Order Block ▼", -1, f"Fiyat aktif arz (bearish) ORDER BLOCK bölgesinde ({fmt_price(rl)} – {fmt_price(rh)})", "OB ▼") - short_refs.append(float(rh)) - - # --- 4) Fair Value Gap retest --- - if A["smc_fvgb"][i] > 0: - vote("FVG ▲", 1, "Fiyat açık yükseliş FVG'sine (dengesizlik boşluğu) geri döndü — destek bölgesi", "FVG ▲") - if A["smc_fvgs"][i] > 0: - vote("FVG ▼", -1, "Fiyat açık düşüş FVG'sine (dengesizlik boşluğu) geri döndü — direnç bölgesi", "FVG ▼") - - # --- 5) Likidite avı --- - sb, ss = A["smc_swb_i"][i], A["smc_sws_i"][i] - if sb >= 0 and i - sb <= SMC_SWEEP_BARS: - vote("Likidite Avı ▲", 1, f"Sell-side likidite alındı ({int(i - sb)} bar önce): dip fitille süpürüldü, fiyat geri döndü", "SWEEP ▲") - long_refs.append(float(A["smc_swb_px"][i])) - if ss >= 0 and i - ss <= SMC_SWEEP_BARS: - vote("Likidite Avı ▼", -1, f"Buy-side likidite alındı ({int(i - ss)} bar önce): tepe fitille süpürüldü, fiyat geri döndü", "SWEEP ▼") - short_refs.append(float(A["smc_sws_px"][i])) - - # --- net teyit + yapı filtresi --- - net = sum(d for _, d, _ in votes) - bull_w = float(sum(1 for _, d, _ in votes if d > 0)) - bear_w = float(sum(1 for _, d, _ in votes if d < 0)) - gate = None - if trend == 0: - score = 0.0 - gate = "Piyasa yapısı henüz belirsiz (onaylı swing kırılımı yok) — SMC sinyali üretilmez" - elif (net > 0 and trend < 0) or (net < 0 and trend > 0): - score = 0.0 - gate = (f"Teyitler {'yükseliş' if net > 0 else 'düşüş'} yönlü ama piyasa yapısı " - f"{'düşüş' if trend < 0 else 'yükseliş'} — yapı dönmeden (CHoCH) işlem yok") - else: - score = float(net) - - regime = "YÜKSELİŞ" if trend > 0 else ("DÜŞÜŞ" if trend < 0 else "BELİRSİZ") - out = { - "score": score, "price": price, "regime": regime, "trend": trend, "net": net, - "bull_w": bull_w, "bear_w": bear_w, "atr_rank": A["atr_rank"][i], "pd": pd_pos, "tags": tags, - "gate": gate, "heat": A["vp_heat"][i], - "stop_ref": {"long": min(long_refs) if long_refs else None, "short": max(short_refs) if short_refs else None}, - } - if verbose: - def tone(d): - return "up" if d > 0 else ("down" if d < 0 else "flat") - bars_txt = "" - if bi >= 0: - bars_txt = f" — son kırılım {int(i - bi)} bar önce ({'CHoCH' if A['smc_brk_choch'][i] > 0 else 'BOS'} {'▲' if A['smc_brk_dir'][i] > 0 else '▼'})" - struct_txt = {1: "Piyasa yapısı YÜKSELİŞ (HH/HL) — yalnızca long aranır", - -1: "Piyasa yapısı DÜŞÜŞ (LH/LL) — yalnızca short aranır", - 0: "Piyasa yapısı belirsiz"}[trend] + (bars_txt if trend else "") - reasons = [(tone(trend), struct_txt)] - reasons += [(tone(d), text) for _, d, text in votes] - if gate: - reasons.append(("flat", gate)) - out["reasons"] = reasons - out["contribs"] = [(lab, float(d)) for lab, d, _ in votes if d != 0] - 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? (long / short kırılımlı)""" - c = df["c"].to_numpy() - n = len(c) - stats = {"all": [0, 0], "long": [0, 0], "short": [0, 0]} - for i in range(start, n - forward): - s = scores[i - start] - if abs(s["score"]) < threshold: - continue - up = s["score"] > 0 - correct = up == (c[i + forward] > c[i]) - for key in ("all", "long" if up else "short"): - 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, - "long_side": pack("long"), "short_side": pack("short"), - } - - -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"], "trend": r["trend"]} - - -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, smc_level=None): - """Stop önceliği: 1) SMC seviyesi (order block / likidite avı ekstremi arkası), 2) yapısal S/R arkası, 3) ATR stop. - Aday stop, fiyata 0.8–3.0 ATR uzaklıktaysa kullanılı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}" - cands = [] - if smc_level is not None: - cands.append((smc_level - d * 0.25 * atr_v, "SMC (OB / likidite arkası)")) - if d > 0 and supports: - cands.append((supports[0][0] - 0.25 * atr_v, "Yapısal (S/R arkası)")) - elif d < 0 and resists: - cands.append((resists[0][0] + 0.25 * atr_v, "Yapısal (S/R arkası)")) - for cand, ctype in cands: - 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, ctype - break - 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): - """Güven = bileşenlerin EŞİT ağırlıklı ortalaması (bileşen ağırlığı yok).""" - mag = min(abs(score) / 5.0, 1.0) - parts = {"Teyit Sayısı": mag, "Teyit Uyumu": agree, "MTF Yapı Teyidi": mtf_state, - "Tarihsel Kenar": hist_q, "Volatilite Rejimi": vol_q} - conf = 100.0 * sum(parts.values()) / len(parts) - 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, use_llm=False): - ind = compute_indicators(df) - n = len(df) - start, scores = compute_score_series(ind, n) - base = _score_at(ind, n - 1, verbose=True) - score, price = base["score"], base["price"] - reasons, contribs = list(base["reasons"]), list(base["contribs"]) - up_c, dn_c = "#3ddc97", "#ff5c72" - tags = [] - if base["trend"] != 0: - tags.append(("YAPI ▲" if base["trend"] > 0 else "YAPI ▼", up_c if base["trend"] > 0 else dn_c)) - for t, d in base["tags"]: - tags.append((t, up_c if d > 0 else dn_c)) - - def note(text): - """Bilgi notu — skoru DEĞİŞTİRMEZ (ağırlık / ek puan yok).""" - reasons.append(("flat", text)) - - # --- 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 yap�� teyidi (üst zaman diliminin piyasa yapısı sinyal yönüyle uyumlu mu?) --- - mtf_info, align_flags = [], [] - for key in ("mtf1", "mtf2"): - m = extras.get(key) - if not m: - continue - aligned = None - if sgn != 0 and m["trend"] != 0: - aligned = bool(m["trend"] == sgn) - align_flags.append(aligned) - nm = "yükseliş" if m["trend"] > 0 else "düşüş" - note(f"{m['bar']} piyasa yapısı {nm} — sinyalle " - + ("uyumlu ✓" if aligned else "ÇELİŞKİLİ ⚠ (GÜÇLÜ sinyal verilmez)")) - mtf_info.append({"bar": m["bar"], "score": round(m["score"], 2), "trend": m["trend"], "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 (bilgi) --- - 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: - note(f"Emir defterinde {'alış' if imb > 0 else 'satış'} baskısı (dengesizlik {imb * 100:+.0f}%)") - - # --- Likidite duvarları (bilgi) --- - 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} - if sup_w: - note(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: - note(f"Yakında büyük satış duvarı {fmt_price(res_w[0])} (medyanın {res_w[1]:.1f}×'i) — direnç olabilir") - - # --- Funding rate (bilgi) --- - fr = extras.get("fund") - if fr is not None and abs(fr) > 0.0002: - side = "long" if fr > 0 else "short" - note(f"Funding {fr * 100:+.3f}% — {side} tarafı kalabalık ({'tasfiye riski' if fr > 0 else 'sıkışma potansiyeli'})") - - # --- Destek / direnç (bilgi + stop yerleşimi) --- - atr_v = float(ind["atr"].iloc[-1]) - supports, resists = find_sr_levels(df, atr_v) - if atr_v > 0 and sgn > 0 and resists and resists[0][1] >= 2 and (resists[0][0] - price) / atr_v <= 1.0: - lv, tc = resists[0] - note(f"Dikkat: yakın direnç {fmt_price(lv)} ({tc} temas, {(lv - price) / atr_v:.1f} ATR) — long hedeflerini sınırlayabilir") - elif atr_v > 0 and sgn < 0 and supports and supports[0][1] >= 2 and (price - supports[0][0]) / atr_v <= 1.0: - lv, tc = supports[0] - note(f"Dikkat: yakın destek {fmt_price(lv)} ({tc} temas, {(price - lv) / atr_v:.1f} ATR) — short hedeflerini sınırlayabilir") - - # --- LLM katmanı (canlı, isteğe bağlı): YALNIZCA yorum — skora etki etmez, backtest'e girmez --- - llm = None - if use_llm: - extra = {"mtf_yapi": [[m["bar"], m["trend"]] for m in mtf_info], - "funding": None if fr is None else round(fr * 100, 4), - "defter_dengesizligi": None if imb is None else round(imb, 2), - "yakin_destek": [[round(lv, 8), tc] for lv, tc in supports[:2]], - "yakin_direnc": [[round(lv, 8), tc] for lv, tc in resists[:2]], - "defter_duvarlari": None if not walls else {k: v for k, v in walls.items() if v}} - snap = _llm_snapshot(df, ind, base, score, contribs, extra) - op, err = llm_opinion(snap, (symbol, bar, str(df["ts"].iloc[-1]), round(score, 1))) - if op: - llm = op - note(f"LLM yorumu [{op.get('source', '?')}]: {op['summary']} (yön {op['bias']:+.2f}, güven %{op['confidence'] * 100:.0f}) — skoru değiştirmez") - if abs(op["bias"]) * op["confidence"] >= 0.1: - tags.append((f"LLM {'▲' if op['bias'] > 0 else '▼'}", up_c if op["bias"] > 0 else dn_c)) - for rk in op["risks"]: - note(f"LLM risk notu: {rk}") - else: - note(f"LLM katmanı atlandı: {err}") - - # --- 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: - note(f"Teyit sayısı güçlü bölgede ancak uyum (%{agree * 100:.0f}) veya MTF yapı 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: - note(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: - ref = base["stop_ref"]["long" if direction > 0 else "short"] - plan = build_trade_plan(price, direction, atr_v, supports, resists, acct, risk_pct, smc_level=ref) - - 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, "mtf": mtf_info, "plan": plan, - "backtest": backtest, "regime": base["regime"], "trend": base["trend"], "pd": base["pd"], - "atr_rank": rank, "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, "walls": walls, "llm": llm, - "smc": ind["smc_zones"], - "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])}, - } - - -# --------------------------------------------------------------------------- -# RISK ANALYTICS -# --------------------------------------------------------------------------- -LLM_SYSTEM = ( - "Sen temkinli bir nicel kripto analistisin. Sana kural tabanlı bir Smart Money Concepts (SMC) sinyal motorunun " - "sayısal özetini JSON olarak veriyorum (piyasa yapısı BOS/CHoCH, order block, fair value gap, likidite avı, " - "premium/discount). Görevin: sinyalin SMC mantığıyla tutarlı olup olmadığını değerlendirmek; yapı ile teyitler " - "arasındaki çelişkileri, geç kalmış girişleri, yakın destek-direnç ve likidite engellerini gözden geçirmek. " - "Skoru körü körüne tekrar etme; veriler çelişiyorsa güveni düşük tut. Yalnızca verilen veriye dayan, haber veya dış bilgi uydurma. " - "SADECE şu JSON'u döndür (başka metin, kod bloğu yok): " - '{"bias": -1..1 arası sayı (-1 güçlü düşüş, +1 güçlü yükseliş), "confidence": 0..1 arası sayı, ' - '"summary": "en fazla 2 cümle Türkçe gerekçe", "risks": ["en fazla 3 kısa Türkçe risk"]}' -) - - -def _llm_snapshot(df, ind, base, score, contribs, extra): - """LLM'e gidecek kompakt, yalnızca sayısal özet (dış metin içermez -> prompt injection yüzeyi yok).""" - last = lambda k: float(ind[k].iloc[-1]) - closes = df["c"].to_numpy(dtype=float) - rets = [round(float(x) * 100, 2) for x in np.diff(closes[-13:]) / closes[-13:-1]] - snap = { - "fiyat": round(float(closes[-1]), 8), "smc_net_teyit": round(float(score), 2), "piyasa_yapisi": base["regime"], - "premium_discount_0_1": None if base["pd"] is None else round(float(base["pd"]), 3), - "smc_teyitler": [[k, int(v)] for k, v in contribs if abs(v) > 1e-9], - "yapi_filtresi_notu": base["gate"], - "rsi": round(last("rsi"), 1), "atr_yuzde": round(last("atr") / closes[-1] * 100, 3), - "atr_yuzdelik": round(float(base["atr_rank"]), 2), "goreli_hacim": round(last("rvol"), 2), - "son_12_bar_getiri_yuzde": rets, - "hacim_profili": {"poc": round(last("vp_poc"), 8), "val": round(last("vp_val"), 8), - "vah": round(last("vp_vah"), 8), - "fiyat_yogunlugu_0_1": None if base["heat"] != base["heat"] else round(float(base["heat"]), 2)}, - } - snap.update(extra) - return snap - - -def _llm_endpoints(): - eps = [] - if LLM_BASE_URL: - eps.append({"url": LLM_BASE_URL, "model": LLM_MODEL, "key": LLM_API_KEY}) - eps.extend(LLM_DEFAULT_ENDPOINTS) - return eps - - -def _llm_url(base): - """Uç nokta tam yol ise (…/chat/completions veya Pollinations'ın …/openai) olduğu gibi, değilse /chat/completions eklenir.""" - base = base.rstrip("/") - return base if base.endswith(("/chat/completions", "/openai")) else base + "/chat/completions" - - -def _llm_pick_model(ep, headers): - """Modeli belirtilmemiş uç noktada GET /models ile ilk modeli seç (10 dk önbellek).""" - def _load(): - r = requests.get(ep["url"].rstrip("/") + "/models", headers=headers, timeout=10) - r.raise_for_status() - items = r.json().get("data") or r.json() - ids = [m.get("id") if isinstance(m, dict) else str(m) for m in items] - ids = [i for i in ids if i] - if not ids: - raise ValueError("model listesi boş") - return ids[0] - return _cached(("llm_model", ep["url"]), 600, _load) - - -def _llm_call(ep, snapshot): - headers = {"content-type": "application/json"} - if ep.get("key"): - headers["authorization"] = f"Bearer {ep['key']}" - model = ep.get("model") or _llm_pick_model(ep, headers) - r = requests.post( - _llm_url(ep["url"]), headers=headers, - json={"model": model, "max_tokens": 500, - "messages": [{"role": "system", "content": LLM_SYSTEM}, - {"role": "user", "content": json.dumps(snapshot, ensure_ascii=False)}]}, - timeout=LLM_TIMEOUT) - r.raise_for_status() - data = r.json() - text = (((data.get("choices") or [{}])[0].get("message") or {}).get("content")) or "" - m = re.search(r"\{.*\}", text, re.DOTALL) - if not m: - raise ValueError("LLM JSON döndürmedi") - d = json.loads(m.group(0)) - bias, conf = float(d["bias"]), float(d["confidence"]) - if not (math.isfinite(bias) and math.isfinite(conf)): - raise ValueError("LLM geçersiz sayı döndürdü") - bias, conf = max(-1.0, min(1.0, bias)), max(0.0, min(1.0, conf)) - risks = [str(x)[:140] for x in (d.get("risks") or [])][:3] - return {"bias": bias, "confidence": conf, "summary": str(d.get("summary", ""))[:400], "risks": risks, - "source": ep["url"].split("/")[2]} - - -def llm_opinion(snapshot, cache_key): - """Uç nokta zincirini sırayla dener. Dönüş: (görüş, None) ya da (None, "host: hata | host: hata").""" - def _load(): - errs = [] - for ep in _llm_endpoints(): - try: - return _llm_call(ep, snapshot) - except Exception as e: - errs.append(f"{ep['url'].split('/')[2]}: {type(e).__name__} {str(e)[:70]}") - raise RuntimeError(" | ".join(errs)) - - try: - return _cached(("llm",) + tuple(cache_key), LLM_CACHE_TTL, _load), None - except Exception as e: - return None, str(e)[:400] - - -def llm_selftest(): - """`python app.py --llm-test` — bu makinede hangi herkese açık uç noktanın çalıştığını gösterir.""" - probe = {"fiyat": 100.0, "skor_motor_ici": 1.5, "rejim": "TREND", "rsi": 55.0} - for ep in _llm_endpoints(): - t0 = time.time() - try: - op = _llm_call(ep, probe) - print(f"[OK ] {ep['url']} {time.time() - t0:.1f}sn bias={op['bias']:+.2f} güven={op['confidence']:.2f}") - except Exception as e: - print(f"[HATA] {ep['url']} {time.time() - t0:.1f}sn {type(e).__name__}: {str(e)[:200]}") - - -if "--llm-test" in sys.argv: - llm_selftest() - sys.exit(0) - - -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'