diff --git "a/app.py" "b/app.py" new file mode 100644--- /dev/null +++ "b/app.py" @@ -0,0 +1,2916 @@ +""" +OKX QUANT TERMINAL · v4 — KNN ANA MOTOR + SMC / VWAP TEYİTLERİ +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. + +v4 — KNN ANA MOTOR + SMC / VWAP TEYİTLERİ + * YÖN = KNN: şimdiki piyasa durumuna (13 özellik: RSI, Stokastik, ADX, EMA/VWAP sapması, momentum, hacim, + premium/discount, piyasa yapısı) en benzer K geçmiş bar bulunur; sonucu BİLİNEN komşuların (5 bar sonra + yükseldi / düştü) çoğunluğu yönü belirler. Havuza yalnızca etiketi o ana kadar bilinen barlar girer (nedensel). + * KNN puanı: komşu payı farkı (edge) ≥%15 → 1, ≥%30 → 2, ≥%50 → 3, ≥%70 → 4 puan. Edge %15'in altındaysa KNN + kararsızdır; bu durumda SMC/VWAP + piyasa yapısı filtresi devralır (KNN_NEUTRAL_FALLBACK). + * TEYİTLER (SMC + VWAP + XGBoost, her biri ±1): taze BOS/CHoCH · premium/discount · order block · FVG retest · likidite avı · + Acheron VWAP etkileşimi · XGBoost (KNN ile aynı özellikler, walk-forward eğitilmiş P(yükseliş)). KNN yönündekiler skoru artırır, ters olanlar azaltır; teyitler yönü ÇEVİREMEZ. + Skor = KNN puanı + net teyit (±11). AL/SAT: |skor| ≥ 2, GÜÇLÜ: |skor| ≥ 4 + uyum + üst zaman dilimi yapısı uyumu. + * DİNAMİK SL/TP: KNN komşularının sinyalden sonraki DYN_HORIZON barda GERÇEKLEŞMİŞ lehte/aleyhte sapmalarından + (MFE/MAE, ATR cinsinden) türetilir: SL = aleyhte sapmanın %75'i, TP1/TP2/TP3 = lehte sapmanın %50/%75/%90'ı. + Yalnızca sonucu o ana kadar bilinen komşular kullanılır (nedensel). Komşu yetersizse sabit SL_ATR/TP_ATR. + * Yedek mod: KNN havuzu yetersizse (az mum) eski SMC kuralı devreye girer (yapı filtreli net teyit sayısı). + * Ü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ı. Canlı sinyal ve backtest AYNI nedensel fonksiyonu kullanır. + * RSI / MACD / Stokastik / ADX grafikleri bilgi amaçlıdır; sinyale dahil değildir. + * RİSK MOTORU (yalnızca Prophet): risk = kullanıcı riski × Prophet çarpanı. Kelly / CVaR / drawdown / P-hit katmanları KALDIRILDI. + Prophet (Meta) fiyat trendini ve volatiliteyi tahmin eder; sinyale ters trend → VETO, volatilite artışı bekleniyorsa pozisyon küçülür. Yalnızca risk azaltır, yön üretmez. +""" +import json +import math +import os +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 +import logging +from html import escape as _html_escape +from statistics import NormalDist +try: # Meta (Facebook) Prophet — isteğe bağlı: kurulu değilse risk motoru Prophet katmanı olmadan çalışır (pip install prophet) + from prophet import Prophet + for _n in ("prophet", "cmdstanpy"): + logging.getLogger(_n).setLevel(logging.WARNING) +except Exception as _pe: + Prophet = None + print(f"[Prophet] 'prophet' paketi yüklenemedi ({str(_pe)[:100]}) → yerleşik Prophet-lite yedeği kullanılacak. Gerçek Prophet için: pip install prophet") + +try: # XGBoost — isteğe bağlı: kurulu değilse scikit-learn gradient boosting yedeği, o da yoksa katman kapalı (pip install xgboost) + from xgboost import XGBClassifier + _SkGB = None +except Exception as _xe: + XGBClassifier = None + try: + from sklearn.ensemble import HistGradientBoostingClassifier as _SkGB + print(f"[XGBoost] 'xgboost' paketi yüklenemedi ({str(_xe)[:80]}) → scikit-learn gradient boosting yedeği kullanılacak. Gerçek XGBoost için: pip install xgboost") + except Exception: + _SkGB = None + print("[XGBoost] ne 'xgboost' ne 'scikit-learn' kurulu → XGBoost teyidi devre dışı (pip install xgboost)") + +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", "POL-USDT", "LTC-USDT", "NEAR-USDT", + "SUI-USDT", "APT-USDT", "ARB-USDT", "OP-USDT", "INJ-USDT", + "BCH-USDT", "ETC-USDT", "XLM-USDT", "ATOM-USDT", "FIL-USDT", + "HBAR-USDT", "ICP-USDT", "VET-USDT", "ALGO-USDT", "AAVE-USDT", + "UNI-USDT", "MKR-USDT", "LDO-USDT", "RUNE-USDT", "GRT-USDT", + "SAND-USDT", "MANA-USDT", "AXS-USDT", "GALA-USDT", "APE-USDT", + "CRV-USDT", "SNX-USDT", "COMP-USDT", "1INCH-USDT", "SUSHI-USDT", + "YFI-USDT", "ZEC-USDT", "DASH-USDT", "XMR-USDT", "EOS-USDT", + "XTZ-USDT", "FLOW-USDT", "THETA-USDT", "CHZ-USDT", "ENJ-USDT", + "BAT-USDT", "ZIL-USDT", "KAVA-USDT", "EGLD-USDT", "FTM-USDT", + "SEI-USDT", "TIA-USDT", "PYTH-USDT", "JUP-USDT", "WLD-USDT", + "ORDI-USDT", "STX-USDT", "IMX-USDT", "RNDR-USDT", "FET-USDT", + "AGIX-USDT", "OCEAN-USDT", "TAO-USDT", "ENA-USDT", "PENDLE-USDT", + "ONDO-USDT", "STRK-USDT", "W-USDT", "ZRO-USDT", "BLUR-USDT", + "DYDX-USDT", "GMX-USDT", "CFX-USDT", "MINA-USDT", "ROSE-USDT", + "KSM-USDT", "QTUM-USDT", "IOTA-USDT", "NEO-USDT", "ONT-USDT", + "WAVES-USDT", "CELO-USDT", "ANKR-USDT", "SKL-USDT", "STORJ-USDT", + "LRC-USDT", "BAND-USDT", "API3-USDT", "JTO-USDT", "PEPE-USDT", + "SHIB-USDT", "FLOKI-USDT", "BONK-USDT", "WIF-USDT", "TRUMP-USDT", + "NOT-USDT", "ETHFI-USDT", "BOME-USDT", "MEME-USDT", "PEOPLE-USDT", + "LUNC-USDT", "SATS-USDT", "CORE-USDT", "ZK-USDT", "IO-USDT", + "BLAST-USDT", "MANTA-USDT", "ALT-USDT", "AEVO-USDT", "OM-USDT", + "WOO-USDT", "MASK-USDT", "AR-USDT", "GAS-USDT", "LQTY-USDT", + "SSV-USDT", "MAGIC-USDT", "ID-USDT", "ARKM-USDT", "CETUS-USDT", + "GOAT-USDT", "PNUT-USDT", "MOVE-USDT", "USUAL-USDT", "VIRTUAL-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ışı +DYNAMIC_EXITS = True # SL/TP'yi KNN komşularının gerçekleşmiş sapmalarından (MFE/MAE) türet; yoksa SL_ATR/TP_ATR +DYN_HORIZON = 12 # komşu sapması: sinyal barından sonraki bu kadar bar (yalnızca sonucu BİLİNEN komşular) +DYN_TP_PCT = 50 # TP1 = komşuların lehte sapmasının bu yüzdeliği (medyan); TP2 = %75, TP3 = %90 +DYN_SL_PCT = 75 # SL = komşuların aleyhte sapmasının bu yüzdeliği +DYN_TP_MIN, DYN_TP_MAX = 1.0, 4.0 # TP1 sınırları (ATR) +DYN_SL_MIN, DYN_SL_MAX = 0.8, 2.5 # SL sınırları (ATR) +DYN_MIN_NB = 8 # sapması bilinen en az komşu sayısı; altındaysa sabit SL_ATR/TP_ATR +# --- Giriş filtreleri (VARSAYILAN KAPALI; yalnızca knn_calibrate.py walk-forward OOS kanıtı bulursa knn_params.json ile açılır) --- +ENTRY_REQUIRE_ALIGN = False # True: piyasa yapısı (BOS/CHoCH trendi) sinyal yönüyle uyumlu değilse işlem yok (karşı-trend elenir) +ENTRY_MIN_ADX = 0.0 # >0: ADX bu değerin altındaysa (trend zayıf / yatay) işlem yok +ENTRY_MAX_FEE_R = 0.0 # >0: tur komisyonunun R cinsinden maliyeti (2·fee / (SL_ATR·ATR/fiyat)) bu değeri aşarsa işlem yok +# --- Pozisyon boyutlandırma: risk = kullanıcı riski × Prophet çarpanı (başka risk katmanı yok) --- +SIZE_RISK_BASE = 0.01 # backtest'te boyutlandırma ve Prophet'siz düz (flat) kıyas riski: işlem başı özkaynağın %1'i +SIZE_MAX_ALLOC = 1.0 # spot: özkaynağın en fazla %100'ü (kaldıraç yok) + +# --- PROPHET RİSK MOTORU (Meta/Facebook Prophet): TEK risk katmanı. Yalnızca risk AZALTIR (çarpan ≤ 1), yön üretmez, risk tavanını aşmaz --- +PROPHET_ENABLED = True # canlı tekil analizde çalışır (tarayıcıda kapalı: hız); PROPHET_BACKTEST açıksa backtest'e de uygulanır +PROPHET_MAX_BARS = 300 # Prophet'e verilen son mum sayısı +PROPHET_MIN_BARS = 120 # bundan az mumda Prophet çalıştırılmaz +PROPHET_HORIZON = 12 # tahmin ufku (bar) — DYN_HORIZON ile uyumlu +PROPHET_INTERVAL = 0.80 # tahmin bandı güven aralığı +PROPHET_CPS = 0.05 # changepoint_prior_scale (düşük = daha sakin trend) +PROPHET_SAMPLES = 200 # belirsizlik örneği (hız / bant kalitesi dengesi) +PROPHET_PFAV_FULL = 0.50 # P(Prophet yönü lehte) ≥ bu → yön çarpanı 1.0 +PROPHET_PFAV_VETO = 0.35 # P(lehte) < bu → VETO (V6): Prophet trendi sinyalin açıkça tersini gösteriyor +PROPHET_MULT_MIN = 0.25 # çarpan tabanı (veto yoksa) +PROPHET_FALLBACK_LITE = True # 'prophet' paketi yoksa yerleşik Prophet-lite (parçalı-doğrusal trend + Fourier mevsimsellik) kullan; False: Prophet katmanı devre dışı +PROPHET_BACKTEST = True # Prophet çarpan/vetosu backtest'e de uygulanır (walk-forward: her PROPHET_BT_STEP barda yeniden eğitilir) +PROPHET_BT_STEP = 12 # backtest'te yeniden eğitim aralığı (bar). Yalnızca o ana kadarki veriyle eğitilir; sinyal barı eğitimden sonraysa ileri tahmin kullanılır +PROPHET_BT_WORKERS = 4 # paralel Prophet eğitimi (sonuçlar önbelleğe alınır; sonraki yenilemelerde yalnızca yeni noktalar eğitilir) +BAR_FREQ = {"1m": "1min", "3m": "3min", "5m": "5min", "15m": "15min", "30m": "30min", "1H": "1h", "2H": "2h", + "4H": "4h", "6H": "6h", "12H": "12h", "1D": "1D", "1W": "7D"} +# --- Formasyon tanıma (VARSAYILAN KAPALI; knn_calibrate.py walk-forward OOS kanıtı bulursa knn_params.json ile açılır) --- +KNN_PATTERN_FEATURES = False # True: KNN özellik vektörüne mum formasyonları + son 3 mumun şekli eklenir (formasyon benzerliği) +PATTERN_VOTES = False # True: en güçlü mum formasyonu ±1 teyit oyu olarak skora eklenir (yönü ÇEVİREMEZ, KNN'e ters ise skoru düşürür) +PAT_WEIGHT = 0.7 # formasyon özelliklerinin KNN mesafesindeki göreli ağırlığı (13 temel özelliği bastırmasın) +PAT_MIN_BODY_ATR = 0.7 # engulfing: gövde en az bu kadar ATR +PAT_PIN_WICK = 2.0 # pin bar: fitil / gövde oranı alt sınırı +PAT_STAR_SMALL = 0.3 # yıldız formasyonu: ortadaki mumun gövdesi en çok bu kadar ATR +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ı +KNN_K = 15 # en yakın komşu sayısı +KNN_FORWARD = 5 # komşunun etiketi: bu kadar bar sonraki getiri (yalnızca sonucu BİLİNEN barlar havuza girer) +KNN_MOVE_ATR = 0.15 # etiket eşiği: ileri getiri > +KNN_MOVE_ATR = yükseliş, < −KNN_MOVE_ATR = düşüş, arası = nötr +KNN_MIN_POOL = 60 # havuzda en az bu kadar geçmiş örnek yoksa KNN oy vermez +KNN_MIN_EDGE = 0.15 # (yükseliş − düşüş komşu payı) bu eşiği aşarsa KNN yön verir +def _knn_tiers(e): + """edge ≥ eşik sırasıyla 1 / 2 / 3 / 4 puan (varsayılan e=0.15 -> 0.15 / 0.30 / 0.50 / 0.70).""" + return (e, min(e + 0.15, 0.85), min(e + 0.35, 0.90), min(e + 0.55, 0.95)) + + +KNN_TIERS = _knn_tiers(KNN_MIN_EDGE) +KNN_POOL_MAX = 1500 # havuz: en fazla son bu kadar etiketli bar (rejim uyumu + hız) +KNN_NEUTRAL_FALLBACK = True # KNN kararsızsa (|edge| < eşik) SMC/VWAP + yapı filtresi devreye girer +SMC_PD_BAND = 0.05 # equilibrium (%50) etrafındaki nötr band +# --- XGBoost teyidi: KNN ile AYNI 13 özellik + AYNI etiket (KNN_FORWARD bar sonrası yön). Yalnızca ±1 TEYİT OYU; yönü ÇEVİREMEZ --- +XGB_ENABLED = True +XGB_STEP = 12 # yeniden eğitim aralığı (bar); eğitim yalnızca o ana kadar SONUCU BİLİNEN barlarla yapılır (walk-forward, nedensel) +XGB_TRAIN_MAX = 800 # eğitim için en fazla son bu kadar etiketli bar +XGB_MIN_TRAIN = 60 # nötr olmayan en az etiketli örnek; altında XGBoost oy vermez +XGB_N_EST, XGB_DEPTH, XGB_LR = 80, 3, 0.08 +XGB_P_VOTE = 0.58 # P(yükseliş) ≥ bu → +1 oy; ≤ 1−bu → −1 oy; arası oy yok + +# --- Kalibrasyon dosyası (knn_calibrate.py üretir; varsa varsayılanların yerine geçer) --- +KNN_PARAMS_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "knn_params.json") +KNN_CALIBRATION = None +if os.path.exists(KNN_PARAMS_FILE): + try: + with open(KNN_PARAMS_FILE, encoding="utf-8") as _f: + KNN_CALIBRATION = json.load(_f) + for _k in ("KNN_K", "KNN_MIN_EDGE", "KNN_FORWARD", "KNN_MOVE_ATR", "KNN_POOL_MAX", "SL_ATR", "TP_ATR", "SIGNAL_THRESHOLD", + "DYNAMIC_EXITS", "DYN_TP_PCT", "DYN_SL_PCT", + "MAX_HOLD_BARS", "ENTRY_REQUIRE_ALIGN", "ENTRY_MIN_ADX", "ENTRY_MAX_FEE_R", + "KNN_PATTERN_FEATURES", "PATTERN_VOTES"): + if _k in KNN_CALIBRATION.get("params", {}): + globals()[_k] = KNN_CALIBRATION["params"][_k] + KNN_TIERS = _knn_tiers(KNN_MIN_EDGE) + TP_R_MULTIPLES = (TP_ATR / SL_ATR, 2.5, 4.0) + print(f"[KNN] kalibrasyon yüklendi: {KNN_CALIBRATION.get('instrument')} {KNN_CALIBRATION.get('bar')} · {KNN_CALIBRATION.get('params')}") + except Exception as _e: + print(f"[KNN] kalibrasyon dosyası okunamadı, varsayılanlar kullanılıyor: {_e}") + + +# --------------------------------------------------------------------------- +# 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 _filter_listed(symbols): + """OKX spot'ta gerçekten listelenen sembolleri tutar (delist / yanlış çift ayıklanır). Ağ hatasında liste olduğu gibi kalır.""" + try: + live = {t["instId"] for t in fetch_all_tickers("SPOT")} + kept = [x for x in symbols if x in live] + return kept if len(kept) >= 20 else symbols + except Exception: + return symbols + + +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 candle_patterns(df, atr_s): + """NEDENSEL mum formasyonu tanıma. Her seri bar i'de YALNIZCA ≤ i verisinden üretilir; +1 boğa, −1 ayı, 0 yok. + engulf : yutan mum (önceki mumun gövdesini tamamen yutan, ≥ PAT_MIN_BODY_ATR·ATR gövdeli ters renkli mum) + pin : çekiç / kayan yıldız (fitil ≥ PAT_PIN_WICK×gövde, fitil ≥ %60 aralık, ters fitil ≤ %25) + önceki 3 barda ters yönlü hareket + star : sabah / akşam yıldızı (3 mum: büyük gövde → küçük gövde → ters yönde ilk mumun yarısını geçen kapanış) + Dönüş: {ad: Series}""" + 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) + + def sh(x, k): + out = np.full(n, np.nan) + if k < n: + out[k:] = x[:n - k] + return out + + body, rng_ = np.abs(c - o), h - l + up_w, lo_w = h - np.maximum(o, c), np.minimum(o, c) - l + o1, c1, o2, c2, body1, body2 = sh(o, 1), sh(c, 1), sh(o, 2), sh(c, 2), sh(body, 1), sh(body, 2) + big = body >= PAT_MIN_BODY_ATR * a + with np.errstate(invalid="ignore"): + bull_eng = (c1 < o1) & (c > o) & (o <= c1) & (c >= o1) & big & (body > body1) + bear_eng = (c1 > o1) & (c < o) & (o >= c1) & (c <= o1) & big & (body > body1) + c4 = sh(c, 4) + down_ctx, up_ctx = c4 > c1, c4 < c1 # i-1'e kadar süren hareket (i-4 → i-1) + ok_rng = (rng_ >= 0.8 * a) & (rng_ > 0) + hammer = ok_rng & (lo_w >= PAT_PIN_WICK * body) & (lo_w >= 0.6 * rng_) & (up_w <= 0.25 * rng_) & down_ctx + shoot = ok_rng & (up_w >= PAT_PIN_WICK * body) & (up_w >= 0.6 * rng_) & (lo_w <= 0.25 * rng_) & up_ctx + small1 = body1 <= PAT_STAR_SMALL * a + morning = (c2 < o2) & (body2 >= 0.6 * a) & small1 & (c > o) & (c > (o2 + c2) / 2) + evening = (c2 > o2) & (body2 >= 0.6 * a) & small1 & (c < o) & (c < (o2 + c2) / 2) + idx = df.index + def ser(bull, bear): + return pd.Series(np.where(bull, 1.0, np.where(bear, -1.0, 0.0)), index=idx) + return {"engulf": ser(bull_eng, bear_eng), "pin": ser(hammer, shoot), "star": ser(morning, evening)} + + +PAT_LABELS = {"engulf": "Yutan mum", "pin": "Pin bar (çekiç/kayan yıldız)", "star": "Sabah/Akşam yıldızı"} + + +def pattern_features(df, ind): + """KNN için formasyon bloğu (n × 7): 3 işaretli formasyon bayrağı + son 3 mumun ATR cinsinden gövdesi + son mumun fitil eğriliği. + Sabit ölçekle bölünüp [-3, 3]'e kırpılır (tüm seri istatistiği yok → sızıntı yok) ve PAT_WEIGHT ile ağırlıklandırılır.""" + atr_ = ind["atr"].replace(0, np.nan) + o, h, l, c = df["o"], df["h"], df["l"], df["c"] + body = (c - o) / atr_ + rng_ = (h - l).replace(0, np.nan) + skew = ((np.minimum(o, c) - l) - (h - np.maximum(o, c))) / rng_ # + = alt fitil uzun (alıcı reddi) + cols = [ind["pat_engulf"], ind["pat_pin"], ind["pat_star"], body, body.shift(1), body.shift(2), skew] + P = np.column_stack([np.clip(np.nan_to_num(x.to_numpy(dtype=float), nan=0.0), -3, 3) for x in cols]) + return P * PAT_WEIGHT + + +def knn_features(df, ind, pattern=None): + """KNN özellik matrisi (n × d). Her özellik SABİT bir ölçekle bölünüp [-3, 3]'e kırpılır: + tüm seri istatistiği kullanılmadığı için ileri sızıntı yoktur. Satır i yalnızca <= i verisinden üretilir.""" + c = df["c"] + atr_ = ind["atr"].replace(0, np.nan) + sh, sl = ind["smc_sh"], ind["smc_sl"] + rng_ = (sh - sl).where(sh > sl) + cols = [ + (ind["rsi"] - 50) / 25, + (ind["stoch_k"] - 50) / 35, + (ind["adx"] - 25) / 15, + (ind["plus_di"] - ind["minus_di"]) / 20, + (c - ind["ema20"]) / atr_ / 2, + (c - ind["ema50"]) / atr_ / 4, + ind["macd_hist"] / atr_ / 0.3, + (c - ind["vwap"]) / atr_ / 2, + (c.diff(3)) / atr_ / 2, + (c.diff(10)) / atr_ / 4, + (ind["rvol"] - 1) / 1.0, + ((c - sl) / rng_ - 0.5) / 0.35, + ind["smc_trend"].astype(float), + ] + F = np.column_stack([np.clip(np.nan_to_num(x.to_numpy(dtype=float), nan=0.0), -3, 3) for x in cols]) + if KNN_PATTERN_FEATURES if pattern is None else pattern: + F = np.hstack([F, pattern_features(df, ind)]) + return F + + +def knn_labels(df, ind, forward=KNN_FORWARD, move=KNN_MOVE_ATR): + """Etiket(j) = j+forward barındaki getirinin yönü (+1 / −1 / 0). Son 'forward' bar için NaN (sonuç henüz bilinmiyor).""" + c = df["c"].to_numpy(dtype=float) + atr_ = ind["atr"].to_numpy(dtype=float) + y = np.full(len(c), np.nan) + if len(c) > forward: + fwd = (c[forward:] - c[:-forward]) / np.where(atr_[:-forward] > 0, atr_[:-forward], np.nan) + y[:-forward] = np.where(np.isnan(fwd), np.nan, np.where(fwd > move, 1.0, np.where(fwd < -move, -1.0, 0.0))) + return y + + +def knn_excursions(df, ind, horizon=DYN_HORIZON): + """Her bar için sonraki 'horizon' barda gerçekleşen azami lehte/aleyhte sapma (ATR cinsinden): + mfe = (max High − Close) / ATR (long için lehte, short için aleyhte) + mae = (Close − min Low) / ATR (short için lehte, long için aleyhte) + Son 'horizon' bar için NaN. Yalnızca SONUCU BİLİNEN komşular için kullanılır (nedensel).""" + h, l, c = (df[k].to_numpy(dtype=float) for k in ("h", "l", "c")) + atr_ = ind["atr"].to_numpy(dtype=float) + n = len(c) + hmax = pd.Series(h[::-1]).rolling(horizon, min_periods=horizon).max().to_numpy()[::-1] # max h[j .. j+H-1] + lmin = pd.Series(l[::-1]).rolling(horizon, min_periods=horizon).min().to_numpy()[::-1] + hmax, lmin = np.r_[hmax[1:], np.nan], np.r_[lmin[1:], np.nan] # max h[j+1 .. j+H] + a = np.where(atr_ > 0, atr_, np.nan) + return (hmax - c) / a, (c - lmin) / a + + +def _xgb_make(): + if XGBClassifier is not None: + return XGBClassifier(n_estimators=XGB_N_EST, max_depth=XGB_DEPTH, learning_rate=XGB_LR, subsample=0.8, colsample_bytree=0.8, + min_child_weight=3, reg_lambda=2.0, objective="binary:logistic", eval_metric="logloss", + random_state=0, n_jobs=1, verbosity=0) + if _SkGB is not None: + return _SkGB(max_iter=XGB_N_EST, max_depth=XGB_DEPTH, learning_rate=XGB_LR, l2_regularization=2.0, random_state=0) + return None + + +def _xgb_name(): + return "XGBoost" if XGBClassifier is not None else ("GradBoost (scikit-learn yedeği)" if _SkGB is not None else "yok") + + +def xgb_walkforward(F, y, n): + """Walk-forward XGBoost: her XGB_STEP barda (mutlak bar numarasına hizalı) model yeniden eğitilir. + Eğitim satırları j ≤ f − KNN_FORWARD (etiketi f anında zaten BİLİNEN barlar); tahmin f…f+STEP−1 barlarına uygulanır. + Böylece bar i için model yalnızca ≤ f ≤ i verisini görür (nedensel) ve veri kesilince aynı bar aynı değeri alır. + Etiket: yükseliş=1 / düşüş=0 (nötr etiketler eğitimden çıkarılır). Dönüş: P(yükseliş) dizisi (hesaplanamayan barlar NaN).""" + p = np.full(n, np.nan) + if not XGB_ENABLED or _xgb_make() is None: + return p + knn_start = WARMUP_BARS + KNN_MIN_POOL + KNN_FORWARD - 1 + f = (max(knn_start, n - BACKTEST_LOOKBACK - XGB_STEP) // XGB_STEP) * XGB_STEP + while f < n: + hi = f - KNN_FORWARD + lo = max(WARMUP_BARS, hi - XGB_TRAIN_MAX + 1) + if hi - lo + 1 >= XGB_MIN_TRAIN: + idx = np.arange(lo, hi + 1) + yy = y[idx] + m = np.isfinite(yy) & (yy != 0) + if m.sum() >= XGB_MIN_TRAIN and len(np.unique(yy[m])) == 2: + try: + mdl = _xgb_make() + mdl.fit(F[idx][m], (yy[m] > 0).astype(int)) + e = min(f + XGB_STEP, n) + p[f:e] = mdl.predict_proba(F[f:e])[:, 1] + except Exception: + pass + f += XGB_STEP + return p + + +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, xgb=False): + 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) + for _k, _v in candle_patterns(df, ind["atr"]).items(): + ind["pat_" + _k] = _v + # --- 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) + arr["knn_F"] = knn_features(df, ind) + arr["knn_y"] = knn_labels(df, ind) + arr["knn_mfe"], arr["knn_mae"] = knn_excursions(df, ind) + arr["xgb_p"] = xgb_walkforward(arr["knn_F"], arr["knn_y"], len(df)) if xgb else np.full(len(df), np.nan) + ind["_arr"] = arr + ind["smc_zones"] = zones + return ind + + +# --------------------------------------------------------------------------- +# COMPOSITE SIGNAL ENGINE v2 (multi-factor, rejim-uyarlamalı, konsensüs filtreli) +# --------------------------------------------------------------------------- +def _dyn_levels(A, nn, i, d): + """KNN komşularının GERÇEKLEŞMİŞ sapmalarından dinamik SL/TP (ATR çarpanı). Yalnızca j + DYN_HORIZON <= i olan komşular.""" + ok = nn[nn <= i - DYN_HORIZON] + mfe, mae = A["knn_mfe"][ok], A["knn_mae"][ok] + fav, adv = (mfe, mae) if d > 0 else (mae, mfe) + fav, adv = fav[~np.isnan(fav)], adv[~np.isnan(adv)] + if len(fav) < DYN_MIN_NB or len(adv) < DYN_MIN_NB: + return None + sl = float(np.clip(np.percentile(adv, DYN_SL_PCT), DYN_SL_MIN, DYN_SL_MAX)) + tp1 = float(np.clip(np.percentile(fav, DYN_TP_PCT), DYN_TP_MIN, DYN_TP_MAX)) + tp1 = max(tp1, sl) # en az 1R + tp2 = float(np.clip(np.percentile(fav, 75), tp1 * 1.25, DYN_TP_MAX * 1.5)) + tp3 = float(np.clip(np.percentile(fav, 90), tp2 * 1.25, DYN_TP_MAX * 2.0)) + return {"dir": int(d), "sl": sl, "tp": tp1, "tp2": tp2, "tp3": tp3, "n": int(len(fav))} + + +def apply_entry_filters(res, A, i): + """Giriş filtreleri. Skor YÖNÜNÜ çevirmez, yalnızca zayıf/pahalı girişleri eler (score=0). Tüm filtreler nedensel (≤ i verisi). + Canlı sinyal, tarayıcı ve backtest bu fonksiyonu _score_at üzerinden AYNI şekilde kullanır; kalibratör önbellekli + filtresiz skorlara bunu sonradan uygular. Dönüş: değişmediyse aynı nesne, elendiyse score=0 olan kopya.""" + s = res["score"] + if s == 0 or not (ENTRY_REQUIRE_ALIGN or ENTRY_MIN_ADX > 0 or ENTRY_MAX_FEE_R > 0): + return res + d = 1 if s > 0 else -1 + why = None + if ENTRY_REQUIRE_ALIGN and res["trend"] * d <= 0: + why = "Filtre: piyasa yapısı sinyal yönüyle uyumlu değil — karşı-trend işlem elendi" + elif ENTRY_MIN_ADX > 0: + adx_i = A["adx"][i] + if not (adx_i >= ENTRY_MIN_ADX): + why = f"Filtre: ADX {adx_i:.0f} < {ENTRY_MIN_ADX:g} — trend zayıf, işlem elendi" + if why is None and ENTRY_MAX_FEE_R > 0: + lv = res.get("levels") + sl_m = lv["sl"] if (lv and lv["dir"] == d) else SL_ATR + a_i, px = A["atr"][i], A["c"][i] + if a_i > 0 and px > 0: + fee_r = 2 * FEE_PER_SIDE / (sl_m * a_i / px) + if fee_r > ENTRY_MAX_FEE_R: + why = f"Filtre: komisyon maliyeti {fee_r:.2f}R > {ENTRY_MAX_FEE_R:g}R — volatilite komisyonu karşılamıyor" + if why is None: + return res + out = dict(res) + out["score"], out["levels"], out["gate"] = 0.0, None, why + if "reasons" in out: + out["reasons"] = list(out["reasons"]) + [("flat", why)] + return out + + +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. + + [YEDEK MOD] YÖN FİLTRESİ (yalnızca KNN kullanılamazsa): 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) + + ANA MOTOR: KNN (bkz. modül başlığı) yönü ve temel puanı (2–4) verir; yukarıdaki 6 teyit net olarak eklenir/çıkarılır. + Skor = KNN puanı·yön + net teyit (−10…+10). 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])) + + # --- 6) Acheron VWAP etkileşimi --- + vw = A["vwap"][i] + if vw == vw: + atr_i = A["atr"][i] if A["atr"][i] == A["atr"][i] else 0.0 + cl = A["c"] + vwv = A["vwap"] + cross_d, cross_age = 0, None + for k in range(0, VWAP_FRESH_BARS + 1): # en yakın taze kesişimi bul (yalnızca geçmiş barlar) + j = i - k + if j < 1 or vwv[j] != vwv[j] or vwv[j - 1] != vwv[j - 1]: + break + if cl[j - 1] <= vwv[j - 1] and cl[j] > vwv[j]: + cross_d, cross_age = 1, k + break + if cl[j - 1] >= vwv[j - 1] and cl[j] < vwv[j]: + cross_d, cross_age = -1, k + break + if cross_d and ((cross_d > 0 and price > vw) or (cross_d < 0 and price < vw)): + when = "bu barda" if cross_age == 0 else f"{cross_age} bar önce" + vote("Acheron VWAP " + ("▲" if cross_d > 0 else "▼"), cross_d, + f"Fiyat VWAP'ı ({fmt_price(vw)}) {when} {'yukarı' if cross_d > 0 else 'aşağı'} kesti ve tutunuyor", + "VWAP ▲" if cross_d > 0 else "VWAP ▼") + elif price > vw and lo_i <= vw + VWAP_TOUCH_ATR * atr_i: + vote("Acheron VWAP ▲", 1, f"Fiyat VWAP'a ({fmt_price(vw)}) geri test yaptı ve üstünde kapandı — dinamik destek", "VWAP ▲") + elif price < vw and hi_i >= vw - VWAP_TOUCH_ATR * atr_i: + vote("Acheron VWAP ▼", -1, f"Fiyat VWAP'a ({fmt_price(vw)}) geri test yaptı ve altında kapandı — dinamik direnç", "VWAP ▼") + + # --- 7) Mum formasyonu (isteğe bağlı teyit; en güçlü TEK formasyon ±1 oy) --- + if PATTERN_VOTES: + for pk in ("engulf", "star", "pin"): # öncelik: yutan > yıldız > pin + pv = int(A["pat_" + pk][i]) + if pv: + vote(PAT_LABELS[pk] + (" ▲" if pv > 0 else " ▼"), pv, + f"{PAT_LABELS[pk]} {'boğa' if pv > 0 else 'ayı'} formasyonu bu barda oluştu", pk.upper() + (" ▲" if pv > 0 else " ▼")) + break + + # --- 8) XGBoost (walk-forward eğitilmiş; KNN ile aynı özellik/etiket) — ±1 teyit oyu, yönü çeviremez --- + xp = float(A["xgb_p"][i]) if XGB_ENABLED else float("nan") + if xp == xp: + if xp >= XGB_P_VOTE: + vote("XGBoost ▲", 1, f"XGBoost: {KNN_FORWARD} bar sonrası yükseliş olasılığı %{xp * 100:.0f}", "XGB ▲") + elif xp <= 1.0 - XGB_P_VOTE: + vote("XGBoost ▼", -1, f"XGBoost: {KNN_FORWARD} bar sonrası düşüş olasılığı %{(1 - xp) * 100:.0f}", "XGB ▼") + + # --- ANA MOTOR: KNN — şimdiki duruma en benzer geçmiş barlar --- + knn = None + F, Y = A["knn_F"], A["knn_y"] + hi_pool = i - KNN_FORWARD # j + forward <= i -> etiket i anında zaten biliniyor + lo_pool = max(WARMUP_BARS, hi_pool - KNN_POOL_MAX + 1) + if hi_pool - lo_pool + 1 >= KNN_MIN_POOL: + pool = np.arange(lo_pool, hi_pool + 1) + dist = np.sqrt(((F[pool] - F[i]) ** 2).sum(axis=1)) + order = np.argsort(dist, kind="stable") + nn = pool[order[:KNN_K]] + ys = Y[nn] + ys = ys[~np.isnan(ys)] + if len(ys) >= max(5, KNN_K // 2): + nb, ns = int((ys > 0).sum()), int((ys < 0).sum()) + edge = (nb - ns) / len(ys) + pts = sum(abs(edge) >= t for t in KNN_TIERS) # 0 = kararsız, 1…4 = yön gücü + knn = {"edge": edge, "n": len(ys), "up": nb, "down": ns, "pts": int(pts), "nn": nn, + "dir": (1 if edge > 0 else -1) if pts else 0} + + # --- skor birleştirme --- + 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 + knn_reason = None + + def smc_only(tag): + """Yalnızca SMC/VWAP net teyit sayısı + piyasa yapısı filtresi (eski kural).""" + if trend == 0: + return 0.0, f"{tag} piyasa yapısı belirsiz — sinyal üretilmez" + if (net > 0 and trend < 0) or (net < 0 and trend > 0): + return 0.0, (f"{tag} 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") + return float(net), f"{tag} yalnızca SMC/VWAP teyitleri kullanıldı" + + if knn is not None: + kd, kp = knn["dir"], knn["pts"] + knn_txt = (f"KNN: en benzer {knn['n']} geçmiş durumdan {knn['up']} tanesi {KNN_FORWARD} bar sonra yükseldi, " + f"{knn['down']} tanesi düştü (net %{knn['edge'] * 100:+.0f})") + if kd == 0: + if KNN_NEUTRAL_FALLBACK: + knn_reason = (0, knn_txt + " — yön vermiyor, SMC/VWAP'a devredildi") + score, gate = smc_only("[KNN kararsız]") + else: + score = 0.0 + knn_reason = (0, knn_txt + " — yön vermiyor") + gate = "KNN ana motoru yön vermiyor (komşular kararsız) — işlem yok" + else: + tags.insert(0, ("KNN ▲" if kd > 0 else "KNN ▼", kd)) + knn_reason = (kd, f"{knn_txt} → {kp} puan") + if kd > 0: + bull_w += kp + else: + bear_w += kp + total = kd * kp + net + if total * kd <= 0: + score = 0.0 + gate = f"SMC/VWAP teyitleri KNN yönüne ters ({net:+d}) — KNN yönü çevrilemez, işlem yok" + else: + score = float(total) + if (trend > 0 and kd < 0) or (trend < 0 and kd > 0): + gate = "Dikkat: piyasa yapısı KNN yönüne ters — işlem karşı-trend" + else: + # YEDEK MOD: KNN için yeterli geçmiş yok + score, gate = smc_only("[Yedek mod: KNN için yeterli geçmiş yok]") + + levels = None + if DYNAMIC_EXITS and knn is not None and knn["dir"] != 0 and score != 0 and (score > 0) == (knn["dir"] > 0): + levels = _dyn_levels(A, knn["nn"], i, knn["dir"]) + + regime = "YÜKSELİŞ" if trend > 0 else ("DÜŞÜŞ" if trend < 0 else "BELİRSİZ") + if knn is not None: + knn = {k: v for k, v in knn.items() if k != "nn"} + out = { + "score": score, "xgb_p": (xp if xp == xp else None), "price": price, "regime": regime, "trend": trend, "net": net, "knn": knn, "levels": levels, + "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 = [] + if knn_reason: + reasons.append((tone(knn_reason[0]), knn_reason[1])) + reasons.append((tone(trend), struct_txt)) + reasons += [(tone(d), text) for _, d, text in votes] + if levels: + reasons.append(("flat", f"Dinamik çıkış (KNN komşuları, {levels['n']} örnek): SL {levels['sl']:.2f}×ATR · " + f"TP {levels['tp']:.2f} / {levels['tp2']:.2f} / {levels['tp3']:.2f}×ATR")) + if gate: + reasons.append(("flat", gate)) + out["reasons"] = reasons + out["contribs"] = [(lab, float(d)) for lab, d, _ in votes if d != 0] + if knn and knn["dir"]: + out["contribs"].insert(0, ("KNN " + ("▲" if knn["dir"] > 0 else "▼"), float(knn["dir"] * knn["pts"]))) + return apply_entry_filters(out, A, i) + + +BACKTEST_MIN_BARS = 40 # KNN havuzu dolduktan sonra backtest penceresinde kalması gereken en az bar + + +def compute_score_series(ind, n): + """Son BACKTEST_LOOKBACK bar için (canlı bar dahil) skor serisi. Dönüş: (başlangıç_indeksi, [skor_dict,...]) + Pencere, KNN havuzunun dolduğu ilk bardan (WARMUP_BARS + KNN_MIN_POOL + KNN_FORWARD − 1) önce BAŞLAMAZ: aksi halde ilk barlar + canlıda kullanılan KNN ana motoru yerine SMC yedek modunda koşar ve backtest iki farklı stratejinin karışımı olur. + Veri bunun için çok kısaysa (canlı sinyal de yedek moddadır) pencere eskisi gibi kalır.""" + start = min(max(WARMUP_BARS, n - BACKTEST_LOOKBACK), n - 1) + knn_start = WARMUP_BARS + KNN_MIN_POOL + KNN_FORWARD - 1 + if n - knn_start >= BACKTEST_MIN_BARS: + start = max(start, knn_start) + 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): + """Sinyal 'forward' bar sonra doğru yönü göstermiş mi? ÜST ÜSTE BİNMEYEN sinyaller sayılır (bir sinyalden sonra + 'forward' bar atlanır) — aksi halde ardışık barların bağımlı sinyalleri doğruluğu ve örnek sayısını şişirir. + Ek olarak yön-ayarlı ortalama getiri (mean_ret) ve komisyon sonrası değeri (net_ret) verilir: doğruluk %58 olsa da + doğru hareketler yanlışlardan küçükse veya tur komisyonunu (2·FEE_PER_SIDE) aşmıyorsa işlem zarar eder.""" + c = df["c"].to_numpy() + n = len(c) + stats = {"all": [0, 0, 0.0], "long": [0, 0, 0.0], "short": [0, 0, 0.0]} + i = start + while i < n - forward: + s = scores[i - start] + if abs(s["score"]) < threshold: + i += 1 + continue + up = s["score"] > 0 + ret = (1 if up else -1) * (c[i + forward] / c[i] - 1) + for key in ("all", "long" if up else "short"): + stats[key][1] += 1 + stats[key][2] += ret + if ret > 0: + stats[key][0] += 1 + i += forward + if stats["all"][1] < 5: + return None + + def pack(key): + ok, t, tot = stats[key] + return {"accuracy": ok / t, "n_signals": t, "mean_ret": tot / t} if t > 0 else None + + mr = stats["all"][2] / stats["all"][1] + return { + "accuracy": stats["all"][0] / stats["all"][1], "correct": stats["all"][0], + "n_signals": stats["all"][1], "forward_bars": forward, + "mean_ret": mr, "net_ret": mr - 2 * FEE_PER_SIDE, + "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] + lv = scores[i - start].get("levels") if DYNAMIC_EXITS else None + sl_m, tp_m = (lv["sl"], lv["tp"]) if lv and lv["dir"] == d else (SL_ATR, TP_ATR) + risk = sl_m * a + sl, tp = entry - d * risk, entry + d * tp_m * 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, + "sl_atr": sl_m, "tp_atr": tp_m, "dyn": bool(lv), "risk_pct": risk / entry}) + 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 + 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, "long": side(1), "short": side(-1), + } + return stats, equity, bh + + +class _LiteProphet: + """'prophet' kurulu değilse yedek. Prophet API'siyle uyumlu (fit / make_future_dataframe / predict): parçalı-doğrusal trend + (ridge'li 10 changepoint) + Fourier gün-içi/haftalık mevsimsellik; aralık = artık sapma · √(1 + 0.15·h). Meta Prophet DEĞİLDİR.""" + def __init__(self, changepoint_prior_scale=0.05, interval_width=0.8, daily_seasonality=False, + weekly_seasonality=False, yearly_seasonality=False, uncertainty_samples=0, **kw): + self.cps, self.w, self.unc = float(changepoint_prior_scale), float(interval_width), uncertainty_samples + self.daily, self.weekly = bool(daily_seasonality), bool(weekly_seasonality) + + @staticmethod + def _sec(ds): + return pd.to_datetime(ds).values.astype("datetime64[s]").astype(np.int64).astype(float) + + def _X(self, ts): + tn = (ts - self.t0) / self.T + cols = [np.ones_like(tn), tn] + [np.maximum(0.0, tn - c) for c in self.cp] + for on, per in ((self.daily, 86400.0), (self.weekly, 7 * 86400.0)): + if on: + for k in (1, 2, 3): + a = 2 * np.pi * k * (ts - self.t0) / per + cols += [np.sin(a), np.cos(a)] + return np.column_stack(cols) + + def fit(self, df): + ts, y = self._sec(df["ds"]), df["y"].to_numpy(dtype=float) + self.t0, self.T = ts[0], max(ts[-1] - ts[0], 1.0) + self.cp = list(np.linspace(0.0, 0.8, 12)[1:-1]) + self.ym, self.ys = float(y.mean()), (float(y.std()) or 1.0) + z = (y - self.ym) / self.ys + X = self._X(ts) + b0 = np.linalg.lstsq(X, z, rcond=None)[0] + s2 = max(float(np.mean((z - X @ b0) ** 2)), 1e-6) + pen = np.zeros(X.shape[1]); pen[2:2 + len(self.cp)] = s2 / (self.cps ** 2) # Gauss önsel sd = changepoint_prior_scale + self.beta = np.linalg.solve(X.T @ X + np.diag(pen) + 1e-6 * np.eye(X.shape[1]), X.T @ z) + self.sd = float(np.std((z - X @ self.beta) * self.ys)) + self.last, self.step, self.ds = ts[-1], float(np.median(np.diff(ts))) if len(ts) > 1 else 3600.0, pd.to_datetime(df["ds"]) + return self + + def make_future_dataframe(self, periods, freq="D", include_history=True): + fut = pd.date_range(self.ds.iloc[-1], periods=periods + 1, freq=freq)[1:] + return pd.DataFrame({"ds": list(self.ds) + list(fut) if include_history else list(fut)}) + + def predict(self, fut): + ts = self._sec(fut["ds"]) + yh = (self._X(ts) @ self.beta) * self.ys + self.ym + out = {"ds": pd.to_datetime(fut["ds"]).values, "yhat": yh} + if self.unc and self.unc > 0: + z = NormalDist().inv_cdf(0.5 + self.w / 2) + h = np.maximum((ts - self.last) / self.step, 0.0) + sdh = self.sd * np.sqrt(1.0 + 0.15 * h) + out["yhat_lower"], out["yhat_upper"] = yh - z * sdh, yh + z * sdh + return pd.DataFrame(out) + + +def _prophet_cls(): + if Prophet is not None: + return Prophet + return _LiteProphet if PROPHET_FALLBACK_LITE else None + + +def _prophet_name(): + return "Prophet" if Prophet is not None else ("Prophet-lite (yerleşik)" if PROPHET_FALLBACK_LITE else "yok") + + +def _prophet_forecast(ds, y, bar, horizon, intervals): + """Prophet'i (ds, y) üzerinde eğitip gelecek 'horizon' bar için tahmin DataFrame'i döndürür (yhat [, yhat_lower, yhat_upper]).""" + span = (ds.iloc[-1] - ds.iloc[0]).total_seconds() / 86400.0 + m = _prophet_cls()(changepoint_prior_scale=PROPHET_CPS, interval_width=PROPHET_INTERVAL, + daily_seasonality=bool(bar not in ("1D", "1W") and span >= 2), + weekly_seasonality=bool(bar != "1W" and span >= 14), yearly_seasonality=False, + uncertainty_samples=PROPHET_SAMPLES if intervals else 0) + m.fit(pd.DataFrame({"ds": ds, "y": y})) + return m.predict(m.make_future_dataframe(periods=horizon, freq=BAR_FREQ.get(bar, "1h"), include_history=False)) + + +def prophet_risk_inputs(df, ind, bar): + """İki Prophet modeli (yön-bağımsız, önbelleğe uygun): + 1) log(kapanış) → ufuktaki beklenen getiri mu_h + fiyat bandı; ölçek = son 100 barın gerçekleşmiş volatilitesi·√H + 2) log(ATR/fiyat) → ufuktaki ort. volatilite / şimdiki volatilite (vol_ratio) — Prophet'in gün-içi mevsimselliği volatilite örüntüsünü yakalar + Dönüş: dict veya {"error": ...} (hata yutulur; motor Prophet'siz devam eder).""" + if _prophet_cls() is None: + return {"error": "prophet kurulu değil (pip install prophet) ve yedek kapalı"} + if len(df) < PROPHET_MIN_BARS: + return {"error": f"Prophet için en az {PROPHET_MIN_BARS} mum gerekir (var: {len(df)})"} + H = PROPHET_HORIZON + try: + d = df.tail(PROPHET_MAX_BARS).reset_index(drop=True) + ds = pd.to_datetime(d["ts"]) + ly = np.log(d["c"].to_numpy(dtype=float)) + fc = _prophet_forecast(ds, ly, bar, H, True) + out = {"mu_h": float(fc["yhat"].iloc[-1] - ly[-1]), "price_fc": float(np.exp(fc["yhat"].iloc[-1])), + "lo": float(np.exp(fc["yhat_lower"].iloc[-1])), "hi": float(np.exp(fc["yhat_upper"].iloc[-1])), + "horizon": H, "vol_ratio": None, "vol_now": None, "vol_fc": None} + rets = np.diff(ly)[-100:] + out["sig_h"] = float(np.std(rets, ddof=1) * math.sqrt(H)) if len(rets) > 5 else float("nan") + av = (ind["atr"].to_numpy(dtype=float) / df["c"].to_numpy(dtype=float))[-len(d):] + ok = np.isfinite(av) & (av > 0) + if ok.sum() >= 60: + fv = _prophet_forecast(ds[ok].reset_index(drop=True), np.log(av[ok]), bar, H, False) + out["vol_now"], out["vol_fc"] = float(av[ok][-1]), float(np.exp(fv["yhat"]).mean()) + out["vol_ratio"] = out["vol_fc"] / out["vol_now"] + return out + except Exception as e: # Prophet/cmdstan hatası canlı sinyali düşürmemeli + return {"error": f"Prophet hatası: {str(e)[:90]}"} + + +def prophet_risk_scale(pf, d): + """Prophet çıktısı + sinyal yönü → risk çarpanı (≤ 1). Yalnızca KÜÇÜLTÜR; yön çevirmez. + yön çarpanı : P(lehte) = Φ(d·mu_h / σ_h). ≥ PROPHET_PFAV_FULL → 1; PROPHET_PFAV_VETO'ya doğru doğrusal azalır; altında VETO (V6) + vol çarpanı : min(1, 1 / vol_ratio) — ufukta volatilite artacaksa pozisyon küçülür (volatilite hedefleme); azalacaksa büyütmez.""" + if not pf or pf.get("error"): + return {"mult": 1.0, "veto": None, "p_fav": None, "dir_scale": 1.0, "vol_scale": 1.0, "error": (pf or {}).get("error"), "inputs": pf} + lo_m = PROPHET_MULT_MIN + sig, p_fav, dir_scale = pf.get("sig_h"), None, 1.0 + if sig and np.isfinite(sig) and sig > 0: + z = d * pf["mu_h"] / sig + p_fav = 0.5 * (1.0 + math.erf(z / math.sqrt(2.0))) + dir_scale = float(np.clip(lo_m + (1 - lo_m) * (p_fav - PROPHET_PFAV_VETO) / (PROPHET_PFAV_FULL - PROPHET_PFAV_VETO), lo_m, 1.0)) + vr = pf.get("vol_ratio") + vol_scale = float(np.clip(1.0 / vr, lo_m, 1.0)) if vr and vr > 0 else 1.0 + veto = f"V6 Prophet tahmini sinyale ters (P(lehte) %{p_fav * 100:.0f})" if (p_fav is not None and p_fav < PROPHET_PFAV_VETO) else None + mult = 0.0 if veto else float(np.clip(dir_scale * vol_scale, lo_m, 1.0)) + return {"mult": mult, "veto": veto, "p_fav": p_fav, "dir_scale": dir_scale, "vol_scale": vol_scale, "error": None, "inputs": pf} + + +_PF_CACHE, _PF_LOCK = {}, threading.Lock() + + +def prophet_backtest_attach(trades, df, ind, bar, symbol=None): + """Her işlemin sinyal barı için NEDENSEL Prophet çarpanı/vetosu hesaplar ve t["prophet"]'a yazar. + Nedensellik: sinyal barı i için eğitim noktası f ≤ i (f = i − (mutlak_bar_no mod STEP)), model YALNIZCA ≤ f verisiyle eğitilir; + f sabit bar numarasına hizalı olduğundan sonuçlar sonraki yenilemelerde yeniden kullanılır. Tahmin f'den ileri uzatılır: + mu_h = yhat[f → i+H] − log(kapanış_i) (i anında bilinen kapanışa göre) · vol_ratio = ort(yhat_vol[i+1…i+H]) / ATR%_i + Not: canlı hesaba göre model en çok STEP−1 bar 'bayat' olabilir (backtest hafifçe kötümser). Dönüş: özet dict veya None.""" + H, R = PROPHET_HORIZON, max(1, int(PROPHET_BT_STEP)) + base_info = {"fits": 0, "covered": 0, "errors": 0, "step": R, "n": len(trades or []), "reason": None, "error": None} + if not trades: + return dict(base_info, reason="backtest penceresinde işlem yok") + if _prophet_cls() is None: + return dict(base_info, reason="prophet kurulu değil ve yedek kapalı (pip install prophet)") + ts = pd.to_datetime(df["ts"]) + dts = np.diff(ts.values.astype("datetime64[s]").astype(np.int64)) + secs = int(np.median(dts)) if len(dts) else 3600 + absi = ts.values.astype("datetime64[s]").astype(np.int64) // max(secs, 1) + c = df["c"].to_numpy(dtype=float) + ly = np.log(c) + av = ind["atr"].to_numpy(dtype=float) / c + need = {} + for t in trades: + i = t["signal_i"] + f = i - int(absi[i] % R) + if f + 1 >= PROPHET_MIN_BARS: + need[i] = f + + def fit_point(f): + key = ("pf", symbol, bar, str(ts.iloc[f]), PROPHET_MAX_BARS, R, H) + with _PF_LOCK: + hit = _PF_CACHE.get(key) + if hit is not None: + return hit + try: + lo = max(0, f + 1 - PROPHET_MAX_BARS) + dsx = ts.iloc[lo:f + 1].reset_index(drop=True) + fp = _prophet_forecast(dsx, ly[lo:f + 1], bar, R + H, False)["yhat"].to_numpy(dtype=float) + a = av[lo:f + 1] + ok = np.isfinite(a) & (a > 0) + fv = None + if ok.sum() >= 60: + fv = np.exp(_prophet_forecast(dsx[ok].reset_index(drop=True), np.log(a[ok]), bar, R + H, False)["yhat"].to_numpy(dtype=float)) + res = {"fp": fp, "fv": fv} + except Exception as e: + res = {"error": f"{type(e).__name__}: {str(e)[:90]}"} + with _PF_LOCK: + _PF_CACHE[key] = res + if len(_PF_CACHE) > 400: + for k in list(_PF_CACHE)[:100]: + _PF_CACHE.pop(k, None) + return res + + fits = {} + uniq = sorted(set(need.values())) + if uniq: + with ThreadPoolExecutor(max_workers=max(1, int(PROPHET_BT_WORKERS))) as ex: + futs = {ex.submit(fit_point, f): f for f in uniq} + for fu in as_completed(futs): + fits[futs[fu]] = fu.result() + covered = errors = 0 + first_err = next((r["error"] for r in fits.values() if r.get("error")), None) + for t in trades: + i = t["signal_i"] + f = need.get(i) + r = fits.get(f) if f is not None else None + if not r or r.get("error"): + errors += 1 if (r and r.get("error")) else 0 + continue + k = i - f + rets = np.diff(ly[max(0, i - 100):i + 1]) + pf = {"mu_h": float(r["fp"][k + H - 1] - ly[i]), + "sig_h": float(np.std(rets, ddof=1) * math.sqrt(H)) if len(rets) > 5 else float("nan"), + "vol_ratio": (float(r["fv"][k:k + H].mean() / av[i]) if (r["fv"] is not None and np.isfinite(av[i]) and av[i] > 0) else None)} + t["prophet"] = prophet_risk_scale(pf, t["dir"]) + covered += 1 + reason = None + if not uniq: + reason = f"işlemler Prophet için çok erken (en az {PROPHET_MIN_BARS} mum gerekir, eldeki: {len(df)})" + elif covered == 0 and first_err: + reason = "eğitim hatası: " + first_err + return dict(base_info, fits=len(uniq), covered=covered, errors=errors, reason=reason, error=first_err) + + +def apply_position_sizing(trades, risk_base=None, max_alloc=None, use_prophet=True): + """PROPHET-ONLY pozisyon boyutlandırma (başka risk katmanı yok). Her işlem için: + risk = risk_base × Prophet çarpanı (≤ 1); Prophet VETO (V6: trend sinyale ters) → işlem açılmaz ('gölge', sonucu izlenir). + Prophet verisi yoksa çarpan 1 (yani sabit risk_base). Pozisyon = min(max_alloc, risk / işlem_riski). + Kıyas: aynı işlemlerde Prophet'siz sabit %risk_base. Dönüş: dict veya None.""" + risk_base = SIZE_RISK_BASE if risk_base is None else risk_base + max_alloc = SIZE_MAX_ALLOC if max_alloc is None else max_alloc + if not trades: + return None + eq = peak = feq = fpeak = 1.0 + dd_max = fdd_max = 0.0 + allocs, sized_pnl, flat_pnl, curve, vetoes = [], [], [], [], {} + for t in sorted(trades, key=lambda x: x["signal_i"]): + rp = t.get("risk_pct") or (abs(t["pnl"] / t["r"]) if t["r"] else 0.0) + pp = t.get("prophet") if use_prophet else None + why, mult = None, 1.0 + if pp: + if pp.get("veto"): + why = pp["veto"] + else: + mult = pp["mult"] + alloc = 0.0 if (why or rp <= 0) else min(max_alloc, risk_base * mult / rp) + if why: + key = why.split(" (")[0] + vetoes[key] = vetoes.get(key, 0) + 1 + fl = min(max_alloc, risk_base / rp) if rp > 0 else 0.0 + ret = alloc * t["pnl"] + eq *= 1 + ret + feq *= 1 + fl * t["pnl"] + peak, fpeak = max(peak, eq), max(fpeak, feq) + dd_max, fdd_max = max(dd_max, 1 - eq / peak), max(fdd_max, 1 - feq / fpeak) + allocs.append(alloc); sized_pnl.append(ret); flat_pnl.append(fl * t["pnl"]); curve.append(eq) + + def pf(x): + x = np.asarray(x) + gl = abs(x[x < 0].sum()) + return float(x[x > 0].sum() / gl) if gl > 0 else (99.0 if x[x > 0].sum() > 0 else 0.0) + + pr_diag = None + cov = [t for t in trades if use_prophet and t.get("prophet")] + if len(cov) >= 5: # Prophet işe yarıyor mu? veto edilenler gerçekten daha mı kötüydü? + vet = [t["r"] for t in cov if t["prophet"]["veto"]] + kept = [t["r"] for t in cov if not t["prophet"]["veto"]] + pr_ = [(t["prophet"]["p_fav"], t["r"]) for t in cov if t["prophet"]["p_fav"] is not None] + corr = None + if len(pr_) >= 8: + a_ = np.asarray(pr_) + if a_[:, 0].std() > 0 and a_[:, 1].std() > 0: + corr = float(np.corrcoef(a_[:, 0], a_[:, 1])[0, 1]) + pr_diag = {"n": len(cov), "n_veto": len(vet), "r_veto": float(np.mean(vet)) if vet else None, + "r_kept": float(np.mean(kept)) if kept else None, "corr": corr} + a = np.asarray(allocs) + return {"total_return": eq - 1, "max_dd": -dd_max, "pf": pf(sized_pnl), "n_active": int((a > 0).sum()), "n": len(a), + "mean_alloc": float(a[a > 0].mean()) if (a > 0).any() else 0.0, "curve": curve, "vetoes": vetoes, + "prophet": pr_diag, "flat_return": feq - 1, "flat_max_dd": -fdd_max, "flat_pf": pf(flat_pnl)} + + +def run_backtest(df, ind, start, scores, bar=None, symbol=None, use_prophet=False): + n = len(df) + if n - start < 20: + return None + acc = directional_accuracy(df, start, scores) + trades = simulate_trades(df, ind, start, scores) + pinfo = prophet_backtest_attach(trades, df, ind, bar, symbol) if (use_prophet and PROPHET_BACKTEST) else None + summ = summarize_trades(trades, df, start) + bt = {"start": start, "acc": acc, "trades": trades, "stats": None, "equity": None, "bh": None, + "sized": apply_position_sizing(trades), "prophet": pinfo} + 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, levels=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 + use_dyn = bool(levels) and levels.get("dir") == d + sl_m = levels["sl"] if use_dyn else SL_ATR + stop = price - d * sl_m * atr_v + stop_type = f"KNN dinamik ({sl_m:.2f}×ATR)" if use_dyn else 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) + if use_dyn and risk > 0: + tp_r = [max(levels[k] * atr_v / risk, 0.5) for k in ("tp", "tp2", "tp3")] + tp_r = [tp_r[0], max(tp_r[1], tp_r[0] * 1.1), max(tp_r[2], tp_r[0] * 1.2)] + tp_r[2] = max(tp_r[2], tp_r[1] * 1.1) + tp_r = [round(m, 2) for m in tp_r] + else: + tp_r = TP_R_MULTIPLES + 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], "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): + ind = compute_indicators(df, xgb=XGB_ENABLED) + 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") + + # --- 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, bar=bar, symbol=symbol, + use_prophet=bool(PROPHET_ENABLED and mtf)) + 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) + + pf_live = None + if PROPHET_ENABLED and mtf: + pkey = ("prophet", symbol, bar, str(df["ts"].iloc[-1]), len(df)) + pf_live = _cached(pkey, 180, lambda: prophet_risk_inputs(df, ind, bar)) + plan, eng = None, None + if not is_neutral and atr_v > 0: + ref = base["stop_ref"]["long" if direction > 0 else "short"] + eff_risk = risk_pct + if PROPHET_ENABLED and risk_pct and risk_pct > 0 and pf_live is not None: + # RİSK MOTORU (yalnızca Prophet): risk = kullanıcı riski × Prophet çarpanı; Prophet VETO (V6) varsa risk 0 (işlem açılmaz). + prp = prophet_risk_scale(pf_live, direction) + veto = prp["veto"] + eff_risk = 0.0 if veto else risk_pct * prp["mult"] + eng = {"prophet": prp, "risk_pct": eff_risk, "user_cap": risk_pct, "veto": veto} + plan = build_trade_plan(price, direction, atr_v, supports, resists, acct, eff_risk, smc_level=ref, levels=base.get("levels")) + + return { + "engine": eng, "prophet_live": pf_live, "xgb_p": base.get("xgb_p"), "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, + "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 +# --------------------------------------------------------------------------- + + + + + + + + + + + + + + + + + + +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'