diff --git "a/app.py" "b/app.py" deleted file mode 100644--- "a/app.py" +++ /dev/null @@ -1,3265 +0,0 @@ -""" -OKX QUANT TERMINAL · v2 — ELITE SIGNAL ENGINE -Hedge-fund-style crypto market analytics dashboard built on Gradio. - -Uses ONLY OKX's public (unauthenticated) market-data REST API: - - /api/v5/market/candles - - /api/v5/market/ticker - - /api/v5/market/tickers - - /api/v5/market/books - - /api/v5/public/funding-rate (türev duyarlılığı, isteğe bağlı) - - /api/v5/public/liquidation-orders (zorunlu tasfiye/likidasyon kümeleri, isteğe bağlı) - -No API key, no order placement, no account access. This is a read-only -analytics / signal-generation terminal, not an execution bot. - -v2 SİNYAL MOTORU — YENİLİKLER - * Yeni faktörler: Supertrend, Ichimoku bulutu, Donchian kırılımı (hacim teyitli), - piyasa yapısı (HH/HL – LH/LL), CMF, MFI, mum formasyonları, Bollinger/Keltner sıkışması - * Rejim ağırlıkları artık ADX ile SÜREKLİ (yumuşak) geçiş yapar (eski: sert eşik) - * Skor tanh ile ±10'a yumuşak sıkıştırılır; "GÜÇLÜ" sinyal için faktör konsensüsü + MTF şartı - * 2 kademeli çoklu zaman dilimi (MTF) teyidi - * Canlı mikro-yapı katmanı: emir defteri dengesizliği, funding rate, yakın destek/direnç - * Yapısal (S/R) stop + ATR stop, 3 kademeli hedef, hesap büyüklüğüne göre pozisyon boyutu - * Gerçek işlem simülasyonu (SL/TP/zaman çıkışı + komisyon): kazanma oranı, profit factor, - beklenti, R çarpanı, SQN, drawdown, Al&Tut karşılaştırması, yarım-Kelly risk önerisi - * Bileşenli GÜVEN skoru + A+ … D kalite notu, faktör katkı grafiği, sinyal işaretli fiyat grafiği - * "Sadece kapanmış mumlar" modu (repaint yok), veri önbelleği, paralel veri çekimi - -v3 SERT FİLTRELER — YÖN İSABETİNİ ARTIRMAYA YÖNELİK (sinyal sayısını azaltıp kaliteyi artırır) - * Rejim kapısı: ADX çok düşükken (belirgin trend yok) sinyal üretilmez - * Konsensüs kapısı: T/R faktörleri yeterince aynı yönde değilse (agree < eşik) sinyal BEKLE'ye düşer - * MTF sert kapısı: üst zaman dilimi net ters yöndeyse normal AL/SAT bile filtrelenir (GÜÇLÜ hariç) - * Emir defteri kapısı: defter sinyale güçlü ters yöndeyse sinyal filtrelenir - * Rejim + konsensüs kapıları canlı sinyal İLE backtest'te AYNI fonksiyonla uygulanır (tutarlılık) - * Backtest artık ilk-yarı/ikinci-yarı isabet oranını karşılaştırıp tutarsızlığı (rejim kayması / - olası aşırı-uyum) işaretliyor ve güven skorunu buna göre düşürüyor - * NOT: Bu filtreler sinyal kalitesini yükseltir ama hiçbir kombinasyon sürekli %80 isabeti garanti - edemez; kripto gibi gürültülü piyasalarda %55-60 tutarlı isabet + iyi R:R zaten güçlü kabul edilir - -v5 LİKİDASYON KÜMELERİ — OKX genel /liquidation-orders uç noktasından (anahtarsız) son - zorunlu tasfiyeler çekilir, fiyat eksenine göre kümelenir (bucket) ve: - * Yakın zamanda ağır basan taraf (long/short tasfiye dengesizliği) kontraryen bir - momentum ipucu olarak kompozit skora küçük bir katkı yapar (funding ile aynı mantık) - * Fiyata yakın büyük kümeler grafikte ve "PİYASA BAĞLAMI"/"LİKİDASYON KÜMELERİ" - kartlarında olası likidite/oynaklık bölgeleri olarak gösterilir (skoru etkilemez) - * Bu katman da MTF/emir defteri/funding gibi CANLIDIR, geçmişe (backtest'e) uygulanmaz - -v6 GENİŞLETİLMİŞ VERİ KATMANI — pozisyonlanma, akıllı para, opsiyon, borsa-arası, makro (hepsi CANLI, backtest'e uygulanmaz) - * Açık Pozisyon (Open Interest): OKX genel /public/open-interest'ten anlık görüntü alınıp - bellekte zaman içinde biriktirilir (OKX geçmiş OI vermez); OI değişimi fiyat yönüyle - birleştirilip "yeni pozisyon mu / kapanış mı" ayrımı yapılır - * Long/Short pozisyon oranı: perakende (rubik/long-short-account-ratio) ve üst-yüzde-5 - "akıllı para" (rubik/…-contract-top-trader) ayrı ayrı çekilir; ikisi ters yöndeyse - "akıllı para ayrışması" olarak işaretlenir - * Taker CVD: rubik/taker-volume ile agresif alış/satış hacmi dengesizliği - * Opsiyon Put/Call açık pozisyon oranı (BTC/ETH): OKX opsiyon OI'sinden -C/-P ayrımıyla - hesaplanır, aşırı uçlarda kontraryen bir duygu göstergesi olarak kullanılır - * Borsalar arası funding uzlaşısı: Binance ve Bybit'in anahtarsız genel uçlarından funding - çekilip OKX'in kendi funding'iyle karşılaştırılır (tek borsaya özgü mü, borsa geneli mi) - * Fear & Greed Index: alternative.me'nin anahtarsız genel API'si, piyasa geneli duygu ölçütü - * Makro rejim: DXY, ABD 10 yıllık tahvil getirisi ve S&P 500'ün günlük yönünden basit bir - risk-on/risk-off özeti (Yahoo Finance'ın belgelenmemiş genel uç noktası — resmi değildir, - başarısız olursa bu katman sessizce atlanır) - NOT: Bu yedi bileşenin tamamı OKX dışındaki (Binance/Bybit/alternative.me/Yahoo Finance) veya - OKX'in "büyük veri" uçlarından gelir; hiçbiri API anahtarı gerektirmez ama resmi/garantili - SLA'ları yoktur — herhangi biri başarısız olursa ilgili kart/katkı sessizce devre dışı kalır. - -v7 CHRONOS KATMANI — önceden eğitilmiş zaman serisi temel modeli (Amazon Chronos-Bolt) ile tahmin - * Yalnızca şu ana kadarki kapanış fiyatlarıyla (nedensel, gelecek sızıntısı yok) çalışır ve - CHRONOS_FORWARD bar ileri için medyan fiyat tahmini (yhat) + %80 belirsizlik bandı (q10-q90) üretir - * Prophet'ten farkı: her çağrıda model eğitilmez (sıfır-atış / zero-shot çıkarım), mevsimsellik - parametresi yoktur; model bir kez yüklenir ve bellekte tutulur - * Tahmin, sinyalin mevcut yönüyle aynıysa skora küçük bir destek, tersse küçük bir ceza olarak - eklenir (NN katmanıyla aynı mantık); katkının büyüklüğü tahminin belirsizlik bandına göre ne - kadar "gürültü dışı" olduğuna göre ölçeklenir - * RSI, Stokastik, MFI, MACD hist, CMF, göreli hacim, ADX ve ATR% de aynı çağrıda (tek batch) tahmin - edilir; tahmin edilen gösterge değerleri fiyat tahminini süzer (her itiraz strength'i çarpar, - toplam çarpan CHRONOS_MIN_FACTOR'ün altına inmez): - - RSI / Stokastik / MFI hedefte aşırı alım-satım bölgesindeyse (geri dönüş riski) - - MACD hist / CMF fiyat yönüne ters işaretliyse (momentum / para akışı teyit etmiyor) - - göreli hacim düşükse, ADX zayıfsa (trend yok), ATR% sıçrayacaksa (belirsizlik artıyor) - NOT: Chronos-Bolt tek değişkenlidir; her seri bağımsız tahmin edilir (birbirini koşullamaz) - * NN gibi CANLI bir katmandır — backtest'e uygulanmaz; chronos-forecasting/torch kurulu değilse veya - yeterli geçmiş veri yoksa bu katman sessizce devre dışı kalır, sinyal motoru yine normal çalışır - -v8 LIGHTGBM KATMANI — NN'in ağaç-tabanlı kardeşi - * NN ile TAM AYNI nedensel öznitelik/etiket şemasını kullanır (forward bar sonra yön yukarı/aşağı), - ama gradyan artırma ağaçlarıyla (LightGBM) eğitilir; iki farklı öğrenme algoritması aynı soruya - bağımsız birer "oy" verir - * Her (sembol, bar) yenilemesinde OTOMATİK yeniden eğitilir (LGBM_CACHE_TTL saniyede bir); lightgbm - kurulu değilse veya yeterli örnek yoksa bu katman sessizce devre dışı kalır - -v9 FinRL + SAC (Soft Actor-Critic) KATMANI — otomatik eğitilen RL ajanı - * FinRL'in sürekli-aksiyon / komisyon-cezalı tek-varlık işlem mantığını izleyen hafif bir özel - Gymnasium ortamında (_SingleAssetTradingEnv), stable-baselines3'ün SAC algoritmasıyla, her - (sembol, bar) için geçmiş barlar üzerinde SIFIRDAN ve OTOMATİK olarak eğitilir - * Ajan hedef pozisyon (-1 tam kısa .. +1 tam uzun) önerir; bu öneri NN/LightGBM/Chronos ile aynı - ölçekte kompozit skora eklenir ve güven skoruna bir bileşen olarak katkı sağlar - * SADECE öğrenilmiş bir öneridir — HİÇBİR gerçek emir GÖNDERMEZ; gymnasium/stable-baselines3 kurulu - değilse veya yeterli geçmiş yoksa bu katman sessizce devre dışı kalır, sinyal motoru normal çalışır - * NN/LightGBM/Chronos'un aksine SAC ajanı artık BACKTEST'e de uygulanır (aynı canlı önbellekli - model, her geçmiş bar için nedensel bir gözlem penceresiyle deterministik olarak yeniden - çalıştırılır). ÖNEMLİ SINIRLAMA: ajan, backtest penceresiyle ÇAKIŞAN veriler üzerinde eğitildiği - için bu gerçek bir walk-forward (eğitim dışı) test DEĞİLDİR — SAC'a atfedilen backtest katkısı - optimist olabilir ve gerçek zamanlı ileriye dönük performansı garanti etmez; bu UI'da açıkça not - edilir. -""" - -import math -import os -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 - -try: # Chronos (torch + chronos-forecasting) kurulu değilse bu katman sessizce devre dışı kalır - import torch - from chronos import BaseChronosPipeline - _CHRONOS_AVAILABLE = True -except Exception as _e: - _CHRONOS_AVAILABLE = False - print(f"[Chronos] import başarısız: {type(_e).__name__}: {_e}", file=sys.stderr) - -try: # LightGBM kurulu değilse bu katman sessizce devre dışı kalır - import lightgbm as lgb - _LGBM_AVAILABLE = True -except Exception as _e: - _LGBM_AVAILABLE = False - print(f"[LightGBM] import başarısız: {type(_e).__name__}: {_e}", file=sys.stderr) - -try: # FinRL + Stable-Baselines3 (SAC) kurulu değilse bu katman sessizce devre dışı kalır. - # Not: FinRL'in kendi StockTradingEnv'i çoklu-varlık / CSV tabanlı bir arayüz varsayar; bu - # panelde tek sembol canlı akış üzerinde çalıştığımız için FinRL'in aksiyon/ödül mantığını - # (sürekli pozisyon, komisyon cezası) izleyen hafif bir özel ortam kullanılır ve ajan bu - # ortam üzerinde stable-baselines3'ün SAC algoritmasıyla OTOMATİK (her yenilemede) eğitilir. - import gymnasium as gym - from gymnasium import spaces as gym_spaces - from stable_baselines3 import SAC - _SAC_AVAILABLE = True - try: - import finrl # noqa: F401 (yalnızca kurulu olup olmadığını UI'da belirtmek için) - _FINRL_AVAILABLE = True - except Exception: - _FINRL_AVAILABLE = False -except Exception as _e: - _SAC_AVAILABLE = False - _FINRL_AVAILABLE = False - print(f"[FinRL/SAC] import başarısız: {type(_e).__name__}: {_e}", file=sys.stderr) - -# --------------------------------------------------------------------------- -# CONFIG -# --------------------------------------------------------------------------- -OKX_BASE = "https://www.okx.com" -TIMEOUT = 10 -CACHE_TTL = 8 # saniye — aynı mum verisini sekmeler arasında tekrar çekmemek için - -POPULAR_SYMBOLS = [ - "BTC-USDT", "ETH-USDT", "SOL-USDT", "BNB-USDT", "XRP-USDT", - "DOGE-USDT", "ADA-USDT", "AVAX-USDT", "LINK-USDT", "TON-USDT", - "TRX-USDT", "DOT-USDT", "MATIC-USDT", "LTC-USDT", "NEAR-USDT", - "SUI-USDT", "APT-USDT", "ARB-USDT", "OP-USDT", "INJ-USDT", -] - -BAR_OPTIONS = ["1m", "3m", "5m", "15m", "30m", "1H", "2H", "4H", "6H", "12H", "1D", "1W"] - -# approximate periods-per-year for each bar size, used to annualize risk stats -BAR_TO_PERIODS_PER_YEAR = { - "1m": 365 * 24 * 60, "3m": 365 * 24 * 20, "5m": 365 * 24 * 12, - "15m": 365 * 24 * 4, "30m": 365 * 24 * 2, "1H": 365 * 24, - "2H": 365 * 12, "4H": 365 * 6, "6H": 365 * 4, "12H": 365 * 2, - "1D": 365, "1W": 52, -} - -CHART_BG = "#0a0e14" - -# her zaman dilimi için "bir üst" zaman dilimi -> çoklu zaman dilimi (MTF) teyidi için -MTF_MAP = { - "1m": "15m", "3m": "30m", "5m": "1H", "15m": "4H", "30m": "4H", - "1H": "4H", "2H": "1D", "4H": "1D", "6H": "1D", "12H": "1W", - "1D": "1W", "1W": "1W", -} -SLOW_BARS = {"4H", "6H", "12H", "1D", "1W"} # bu vadelerde emir defteri etkisi yarıya iner - -# --- Sinyal motoru parametreleri --- -SIGNAL_THRESHOLD = 2.0 # |skor| >= bu değer -> AL / SAT -STRONG_THRESHOLD = 4.5 # |skor| >= bu değer + konsensüs -> GÜÇLÜ -WARMUP_BARS = 80 # Ichimoku (52+26) ısınması için minimum bar -BACKTEST_LOOKBACK = 220 # walk-forward test penceresi (bar) -SL_ATR, TP_ATR = 1.5, 2.5 # backtest ve varsayılan işlem planı (ATR çarpanları) -MAX_HOLD_BARS = 24 # zaman çıkışı -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) - -# --- v3 SERT FİLTRELER (gate) — düşük kaliteli sinyalleri eleyerek isabet oranını artırır --- -# Not: bu eşikler sinyal SAYISINI azaltır, isabet oranını yükseltmeye çalışır. %80 gibi bir -# oranın SÜREKLİ garanti edilebileceği bir gösterge kombinasyonu yoktur; bu filtreler sadece -# düşük güvenilirlikli barları "BEKLE/NÖTR"e çevirerek kalan sinyallerin ortalama kalitesini yükseltir. -MIN_REGIME_ADX = 12 # ADX bu değerin altında ve sıkışma yoksa -> belirgin trend yok, sinyal verilmez -MIN_AGREE_FOR_SIGNAL = 0.55 # faktör konsensüsü (agree) bu oranın altındaysa sinyal BEKLE'ye düşer -MTF_HARD_GATE = True # üst zaman dilimi (mtf1) net ters yöndeyse normal AL/SAT bile filtrelenir -OB_HARD_GATE_IMB = 0.35 # emir defteri dengesizliği sinyale bu kadar ters ve güçlüyse sinyal filtrelenir -STABILITY_MIN_SAMPLES = 12 # ilk-yarı/ikinci-yarı isabet stabilite kontrolü için min. sinyal sayısı -STABILITY_MAX_GAP = 0.30 # iki yarı arasındaki isabet farkı bunu aşarsa "tutarsız" (rejim kayması) işaretlenir - -# --- v4 SİNİR AĞI (hafif, NumPy-only MLP) — kompozit skora öğrenilmiş bir yön-olasılığı katmanı ekler --- -NN_HIDDEN = 12 # gizli katman nöron sayısı -NN_EPOCHS = 300 # tam-batch gradient descent iterasyonu -NN_LR = 0.05 # öğrenme oranı -NN_L2 = 1e-3 # L2 düzenlileştirme (ezberlemeyi azaltmak için) -NN_FORWARD = 5 # etiket ufku: forward bar sonra yön yukarı mı aşağı mı (directional_accuracy ile aynı) -NN_MIN_TRAIN = 60 # bu kadar eğitim örneği altında model hiç kullanılmaz (güvenilmez) -NN_WEIGHT = 1.6 # NN'in kompozit skora katkısının maksimum büyüklüğü (diğer faktörlerle aynı ölçek) -NN_GATE_STRENGTH = 0.5 # NN, sinyale bu kadar (0-1) güçlü ters düşerse sinyal sert filtreyle elenir -NN_CACHE_TTL = 45 # saniye — aynı (sembol, bar) için modeli bu süre boyunca yeniden eğitme - -# --- Chronos (v7) zaman serisi tahmin parametreleri --- -CHRONOS_MODEL = "amazon/chronos-bolt-small" # tiny/mini/small/base — CPU'da small hızlı ve yeterli -CHRONOS_MIN_TRAIN = 80 # bu kadar bar altında Chronos hiç kullanılmaz -CHRONOS_CONTEXT = 512 # modele verilecek en fazla geçmiş bar sayısı -CHRONOS_FORWARD = 24 # kaç bar ileriye tahmin (Bolt için <= 64 tek adımda üretilir) -CHRONOS_WEIGHT = 1.4 # Chronos'un kompozit skora katkısının maksimum büyüklüğü (diğer faktörlerle aynı ölçek) -CHRONOS_GATE_STRENGTH = 0.5 # Chronos, sinyale bu kadar (0-1) güçlü ters düşerse sert filtreyle elenir -CHRONOS_RSI_OB = 70 # Chronos'un tahmin ettiği RSI bu değerin üstündeyse yukarı tahmin zayıflatılır -CHRONOS_RSI_OS = 30 # ... bu değerin altındaysa aşağı tahmin zayıflatılır (hedef bölge aşırı alım/satım) -CHRONOS_RSI_DAMP = 0.5 # aşırı bölgeye varan tahminin strength çarpanı -CHRONOS_STOCH_OB = 80 # tahmin edilen Stokastik %K bu değerin üstündeyse yukarı tahmin zayıflatılır -CHRONOS_STOCH_OS = 20 # ... bu değerin altındaysa aşağı tahmin zayıflatılır -CHRONOS_MFI_OB = 80 # MFI için aynı aşırı alım / satım eşikleri -CHRONOS_MFI_OS = 20 -CHRONOS_CMF_MIN = 0.05 # tahmin edilen CMF yön ile ters ve |CMF| bunun üstündeyse para akışı teyit etmiyor -CHRONOS_RVOL_MIN = 0.6 # tahmin edilen göreli hacim (hacim / 20 bar ort.) bunun altındaysa hareket ince hacimde -CHRONOS_ADX_MIN = 20 # tahmin edilen ADX bunun altındaysa trend zayıf -CHRONOS_ATR_SPIKE = 1.5 # tahmin edilen ATR% / mevcut ATR% bu orana varırsa volatilite sıçraması -CHRONOS_FILTER_DAMP = 0.8 # RSI dışındaki her süzgeç itirazının strength çarpanı -CHRONOS_MIN_FACTOR = 0.25 # tüm süzgeçlerin toplam çarpanı bunun altına inmez -CHRONOS_CACHE_TTL = 60 # saniye — aynı (sembol, bar) için tahmini bu süre boyunca yeniden üretme - -# --- v8 LightGBM (gradyan artırma ağaçları) — NN'e ek, ağaç-tabanlı öğrenilmiş yön katmanı --- -LGBM_FORWARD = 5 # etiket ufku: NN ile aynı (forward bar sonra yön yukarı mı aşağı mı) -LGBM_MIN_TRAIN = 80 # bu kadar eğitim örneği altında model hiç kullanılmaz -LGBM_WEIGHT = 1.5 # LightGBM'in kompozit skora katkısının maksimum büyüklüğü -LGBM_GATE_STRENGTH = 0.5 # LightGBM, sinyale bu kadar (0-1) güçlü ters düşerse sert filtreyle elenir -LGBM_CACHE_TTL = 45 # saniye — aynı (sembol, bar) için modeli bu süre boyunca yeniden eğitme -LGBM_PARAMS = dict( # küçük/hızlı ağaçlar — canlı panelde her yenilemede yeniden eğitilebilsin diye - n_estimators=150, max_depth=4, num_leaves=15, learning_rate=0.05, - subsample=0.8, colsample_bytree=0.8, min_child_samples=10, - reg_alpha=0.1, reg_lambda=0.1, objective="binary", verbosity=-1, n_jobs=1, -) - -# --- v9 FinRL + SAC (Soft Actor-Critic) — otomatik eğitilen RL ajanı, hedef pozisyon önerir --- -# NOT: Gerçek FinRL kurulumu (StockTradingEnv) çoklu-varlık/CSV odaklıdır; burada onun sürekli- -# aksiyon/komisyon-cezalı mantığını izleyen hafif bir özel ortam kullanılır (bkz. _SingleAssetTradingEnv). -# Ajan HİÇBİR gerçek emir GÖNDERMEZ; yalnızca kompozit skora bir "öğrenilmiş pozisyon önerisi" katkısı sağlar. -SAC_WINDOW = 20 # ajanın gözlem penceresi (son kaç barın özniteliği) -SAC_MIN_TRAIN = 150 # bu kadar bar altında ortam/ajan hiç eğitilmez -SAC_TRAIN_TIMESTEPS = 8000 # her (sembol, bar) için otomatik eğitim adımı sayısı (4000 -> 8000: 0'a kilitlenmeyi azaltmak için) -SAC_WEIGHT = 1.7 # SAC ajanının kompozit skora katkısının maksimum büyüklüğü -SAC_GATE_STRENGTH = 0.5 # SAC, sinyale bu kadar (0-1) güçlü ters düşerse sert filtreyle elenir -SAC_CACHE_TTL = 120 # saniye — RL eğitimi pahalı olduğundan NN/LightGBM'den daha uzun önbelleklenir -SAC_FEE = FEE_PER_SIDE # ortamdaki komisyon cezası, canlı motorla tutarlı tutulur -SAC_REWARD_SCALE = 100 # ödülü büyütür (oranı DEĞİŞTİRMEZ): ret ve fee ile aynı bar getirisi - # mertebesinde (~0.001) kaldığında kritik ağ sinyali gürültüden ayıramıyor - # ve politika 0'a (işlem açmama) kilitleniyordu - -# --- v5 LİKİDASYON KÜMELERİ — zorunlu tasfiye (liquidation) akışını sinyal motoruna ekler --- -# OKX'in genel (public) /liquidation-orders uç noktası kullanılır; anahtar gerekmez, sadece -# son birkaç günün "filled" (gerçekleşmiş) tasfiyelerini döndürür. Bu veriyle fiyat eksenine -# göre kümeler (bucket) oluşturulur: her kümenin toplam nominal büyüklüğü ve long/short yönü -# hesaplanır. Kullanım alanları: -# 1) Yakın zamanda ağır basan taraf (long mu short mu tasfiye oldu) kontraryen bir momentum -# ipucu olarak kompozit skora küçük bir katkı yapar (funding rate mantığıyla tutarlı). -# 2) Fiyata yakın büyük kümeler potansiyel mıknatıs/likidite bölgesi olarak S/R gibi -# grafikte ve bağlam kartlarında gösterilir (skora doğrudan etki etmez, bilgi amaçlıdır). -LIQ_INST_TYPE = "SWAP" # tasfiye verisi sadece türev ürünlerde vardır (spot sembolün SWAP karşılığı kullanılır) -LIQ_LOOKBACK = 100 # API'den istenecek maksimum tasfiye kaydı -LIQ_CACHE_TTL = 20 # saniye — aynı sembol için tasfiye verisini bu süre boyunca yeniden çekme -LIQ_CLUSTER_BINS = 40 # fiyat aralığının kaç dilime bölünerek kümeleneceği -LIQ_CLUSTER_MAX_LEVELS = 6 # gösterilecek/en büyük kaç küme tutulsun -LIQ_MIN_TOTAL_NOTIONAL = 5000 # bu tutarın (USDT) altındaki toplam tasfiye hacmi anlamsız kabul edilip atlanır -LIQ_IMBALANCE_MIN = 0.25 # long/short tasfiye dengesizliği bu eşiği aşarsa skora katkı yapılır -LIQ_IMBALANCE_WEIGHT = 1.2 # dengesizliğin skora katkısının maksimum büyüklüğü -LIQ_PROXIMITY_ATR = 1.2 # fiyata bu kadar ATR mesafedeki kümeler "yakın" kabul edilir (bağlam/grafik için) - -# --- v6 GENİŞLETİLMİŞ VERİ KATMANI — pozisyonlanma, akıllı para, opsiyon, borsa-arası, makro --- -OI_CACHE_TTL = 15 # saniye — açık pozisyon (open interest) anlık görüntüsü -OI_HISTORY_MAX = 40 # OI değişimini ölçmek için bellekte tutulan son kaç anlık görüntü -OI_DELTA_MIN_PCT = 1.5 # OI bu yüzdenin üzerinde değişmezse skora katkı yapılmaz -OI_WEIGHT = 1.0 # OI + fiyat kombinasyonunun skora katkısının maksimum büyüklüğü - -LS_RATIO_PERIOD = "5m" # long/short oranı granülaritesi -LS_CACHE_TTL = 30 -LS_RETAIL_EXTREME = 2.0 # perakende L/S oranı bu değerin üzerinde/altında (ya da 1/bu) "kalabalık" sayılır -LS_DIVERGENCE_WEIGHT = 1.0 # perakende vs üst-yüzde-5 trader ayrışmasının skora katkısı - -TAKER_VOL_PERIOD = "5m" -TAKER_CACHE_TTL = 20 -TAKER_CVD_MIN_RATIO = 0.15 # taker alış/satış dengesizliği bu oranı aşarsa skora katkı yapılır -TAKER_CVD_WEIGHT = 0.8 - -OPT_PCR_CACHE_TTL = 60 -OPT_PCR_EXTREME_HIGH = 1.3 # put/call OI oranı bu değerin üzerindeyse aşırı korku (kontraryen boğa) -OPT_PCR_EXTREME_LOW = 0.6 # bu değerin altındaysa aşırı iyimserlik (kontraryen ayı) -OPT_PCR_WEIGHT = 0.7 - -CROSS_FUNDING_CACHE_TTL = 30 -CROSS_FUNDING_WEIGHT = 0.3 # OKX funding'inin borsalar arası uzlaşıya göre teyit/çelişki katkısı - -FNG_CACHE_TTL = 300 # Fear & Greed Index günde birkaç kez güncellenir -FNG_EXTREME_FEAR = 25 -FNG_EXTREME_GREED = 75 -FNG_WEIGHT = 0.6 - -MACRO_CACHE_TTL = 300 -MACRO_WEIGHT = 0.8 # DXY/ABD 10Y/SPX rejim uyumunun skora katkısı (risk varlıklarında anlamlı) - - -# --------------------------------------------------------------------------- -# 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) - - -def fetch_liquidation_orders(inst_id, inst_type=LIQ_INST_TYPE, state="filled", limit=LIQ_LOOKBACK): - """Son zorunlu tasfiye (liquidation) emirlerini OKX genel API'sinden çeker. - Anahtar gerektirmez; sadece son birkaç günü kapsayan sınırlı bir geçmiş döner. - Spot sembolün (ör. BTC-USDT) türev karşılığı (uly) üzerinden sorgulanır.""" - parts = inst_id.split("-") - if len(parts) != 2: - return [] - - def _load(): - uly = f"{parts[0]}-{parts[1]}" - try: - raw = _get("/api/v5/public/liquidation-orders", - {"instType": inst_type, "uly": uly, "state": state, "limit": limit}) - except Exception: - return [] - out = [] - for grp in raw or []: - for d in grp.get("details", []) or []: - try: - px = float(d.get("bkPx") or 0) - sz = float(d.get("sz") or 0) - except (TypeError, ValueError): - continue - if px <= 0 or sz <= 0: - continue - out.append({ - "px": px, "sz": sz, - "side": d.get("side"), # 'sell' -> long tasfiyesi, 'buy' -> short tasfiyesi - "pos_side": d.get("posSide"), - "ts": int(d.get("ts") or 0), - }) - return out - - return _cached(("liq", inst_id, inst_type, state), LIQ_CACHE_TTL, _load) - - -# --------------------------------------------------------------------------- -# v6 GENİŞLETİLMİŞ VERİ KAYNAKLARI -# İlk grup (OI, L/S oranı, taker hacmi, opsiyon PCR) OKX'in kendi genel "büyük veri" (rubik) -# uçlarından gelir. İkinci grup (borsalar arası funding, Fear&Greed, makro) OKX dışı, anahtarsız -# genel uçlar kullanır — bunlar resmi/garantili değildir, bu yüzden her biri kendi try/except'i -# içinde izole edilmiştir: biri başarısız olursa yalnızca o bileşen sessizce atlanır. -# --------------------------------------------------------------------------- -def _get_external(url, params=None, headers=None, retries=1, timeout=TIMEOUT): - """OKX dışı (Binance/Bybit/alternative.me/Yahoo Finance) anahtarsız genel uçlar için - genel amaçlı GET yardımcı fonksiyonu. _get()'ten farkı: OKX'in {code,msg,data} zarfını - beklemez, ham JSON'u döner.""" - last_err = None - for attempt in range(retries + 1): - try: - r = requests.get(url, params=params, headers=headers, timeout=timeout) - r.raise_for_status() - return r.json() - except requests.exceptions.RequestException as e: - last_err = e - if attempt < retries: - time.sleep(0.3 * (attempt + 1)) - continue - raise RuntimeError(f"Harici veri kaynağı hatası: {last_err}") - - -def fetch_open_interest(inst_id): - """Açık pozisyon (open interest): spot sembolün SWAP karşılığı için anlık kontrat/coin - cinsinden değer. OKX bu uç noktada geçmiş vermez — kısa vadeli değişimi ölçmek için - track_oi_history() ile zaman içinde ayrıca biriktirilir.""" - parts = inst_id.split("-") - if len(parts) != 2: - return None - - def _load(): - try: - raw = _get("/api/v5/public/open-interest", {"instType": "SWAP", "instId": f"{inst_id}-SWAP"}) - except Exception: - return None - if not raw: - return None - d = raw[0] - try: - return {"oi_ccy": float(d.get("oiCcy") or 0), "ts": int(d.get("ts") or 0)} - except (TypeError, ValueError): - return None - - return _cached(("oi", inst_id), OI_CACHE_TTL, _load) - - -_OI_HISTORY = {} -_OI_HISTORY_LOCK = threading.Lock() - - -def track_oi_history(inst_id, oi_snapshot): - """OI anlık görüntülerini sembol başına bellekte biriktirir ve elde tutulan en eski/en yeni - örnek arasındaki % değişimi döner (OKX'in genel API'si OI geçmişi sağlamadığı için kendi - örneklememiz gerekir). Süreç yeniden başlarsa geçmiş de sıfırlanır — bu normaldir.""" - if not oi_snapshot: - return None - with _OI_HISTORY_LOCK: - hist = _OI_HISTORY.setdefault(inst_id, []) - if not hist or hist[-1]["ts"] != oi_snapshot["ts"]: - hist.append(oi_snapshot) - if len(hist) > OI_HISTORY_MAX: - del hist[: len(hist) - OI_HISTORY_MAX] - if len(hist) < 2 or hist[0]["oi_ccy"] <= 0: - return {"oi": hist[-1]["oi_ccy"], "change_pct": None, "n_samples": len(hist)} - first, last = hist[0], hist[-1] - chg = (last["oi_ccy"] - first["oi_ccy"]) / first["oi_ccy"] - return {"oi": last["oi_ccy"], "change_pct": chg, "n_samples": len(hist)} - - -def _parse_ts_ratio_row(raw): - """rubik uçlarının [ts, oran] (bazen dict) satır formatını toleranslı biçimde ayrıştırır.""" - if not raw: - return None - row = raw[0] - try: - if isinstance(row, (list, tuple)): - return float(row[1]) - if isinstance(row, dict): - for key in ("longShortAcctRatio", "ratio", "longShortRatio"): - if key in row: - return float(row[key]) - return float(list(row.values())[-1]) - except (TypeError, ValueError, IndexError): - return None - return None - - -def fetch_long_short_ratio(ccy, period=LS_RATIO_PERIOD): - """Perakende hesap sayısına göre long/short oranı (borsa geneli, sembol değil kripto bazlı). - >1.0: long hesap sayısı fazla, <1.0: short hesap sayısı fazla.""" - def _load(): - try: - raw = _get("/api/v5/rubik/stat/contracts/long-short-account-ratio", {"ccy": ccy, "period": period}) - except Exception: - return None - return _parse_ts_ratio_row(raw) - - return _cached(("ls_ratio", ccy, period), LS_CACHE_TTL, _load) - - -def fetch_long_short_ratio_top_trader(inst_id, period=LS_RATIO_PERIOD): - """Elit / üst-yüzde-5 trader'ların long/short pozisyon oranı — 'akıllı para' göstergesi. - Perakende oranıyla karşılaştırılıp ayrışma sinyaline dönüştürülür.""" - parts = inst_id.split("-") - if len(parts) != 2: - return None - - def _load(): - try: - raw = _get("/api/v5/rubik/stat/contracts/long-short-account-ratio-contract-top-trader", - {"instId": f"{inst_id}-SWAP", "period": period}) - except Exception: - return None - return _parse_ts_ratio_row(raw) - - return _cached(("ls_ratio_top", inst_id, period), LS_CACHE_TTL, _load) - - -def fetch_taker_volume(ccy, inst_type="SPOT", period=TAKER_VOL_PERIOD): - """Taker (agresif) alış/satış hacmi -> kısa vadeli CVD (cumulative volume delta) benzeri - bir alıcı/satıcı baskı ölçütü için ham alış/satış hacmini döner.""" - def _load(): - try: - raw = _get("/api/v5/rubik/stat/taker-volume", {"ccy": ccy, "instType": inst_type, "period": period}) - except Exception: - return None - if not raw: - return None - row = raw[0] - try: - if isinstance(row, dict): - sell_vol, buy_vol = float(row.get("sellVol") or 0), float(row.get("buyVol") or 0) - else: - sell_vol, buy_vol = float(row[1]), float(row[2]) - except (TypeError, ValueError, IndexError): - return None - return {"sell": sell_vol, "buy": buy_vol} - - return _cached(("taker_vol", ccy, inst_type, period), TAKER_CACHE_TTL, _load) - - -def fetch_option_pcr(base_ccy): - """BTC/ETH opsiyonlarında toplam açık pozisyona göre put/call oranı (piyasa duygusu - göstergesi). Yalnızca OKX'te aktif bir opsiyon piyasası olan kripto paralar için anlamlıdır; - diğerlerinde OI listesi boş döner ve bu katman otomatik atlanır.""" - def _load(): - try: - raw = _get("/api/v5/public/open-interest", {"instType": "OPTION", "uly": f"{base_ccy}-USD"}) - except Exception: - return None - if not raw: - return None - call_oi = put_oi = 0.0 - for d in raw: - inst = d.get("instId", "") - try: - oi = float(d.get("oiCcy") or 0) - except (TypeError, ValueError): - continue - if inst.endswith("-C"): - call_oi += oi - elif inst.endswith("-P"): - put_oi += oi - if call_oi <= 0: - return None - return {"put_oi": put_oi, "call_oi": call_oi, "pcr": put_oi / call_oi} - - return _cached(("opt_pcr", base_ccy), OPT_PCR_CACHE_TTL, _load) - - -def fetch_cross_exchange_funding(base_ccy): - """Binance ve Bybit'in aynı sembol için funding oranını çeker (anahtarsız genel uçlar); - OKX'in kendi funding'iyle karşılaştırarak pozisyonlanmanın tek borsaya mı özgü yoksa - borsa geneli mi olduğunu ayırt etmeye yardımcı olur. Her borsa ayrı try/except içindedir.""" - sym = f"{base_ccy}USDT" - - def _load(): - out = {} - try: - raw = _get_external("https://fapi.binance.com/fapi/v1/premiumIndex", {"symbol": sym}) - if raw: - out["binance"] = float(raw.get("lastFundingRate") or 0.0) - except Exception: - pass - try: - raw = _get_external("https://api.bybit.com/v5/market/tickers", {"category": "linear", "symbol": sym}) - lst = ((raw or {}).get("result") or {}).get("list") or [] - if lst: - out["bybit"] = float(lst[0].get("fundingRate") or 0.0) - except Exception: - pass - return out or None - - return _cached(("cross_funding", base_ccy), CROSS_FUNDING_CACHE_TTL, _load) - - -def fetch_fear_greed(): - """Crypto Fear & Greed Index (alternative.me) — piyasa geneli duygu ölçütü, anahtar - gerektirmez, günde birkaç kez güncellenir.""" - def _load(): - try: - raw = _get_external("https://api.alternative.me/fng/", {"limit": 1}) - except Exception: - return None - data = (raw or {}).get("data") or [] - if not data: - return None - try: - return {"value": int(data[0]["value"]), "label": data[0].get("value_classification", "")} - except (KeyError, ValueError, TypeError): - return None - - return _cached(("fng",), FNG_CACHE_TTL, _load) - - -def _yahoo_last_change(symbol): - """Yahoo Finance'ın anahtarsız 'chart' uç noktasından son iki günlük kapanışı çekip - günlük % değişimi hesaplar. Resmi/belgelenmiş bir API değildir — başarısız olursa - sessizce None döner, bu yüzden makro katman tamamen opsiyoneldir.""" - try: - raw = _get_external( - f"https://query1.finance.yahoo.com/v8/finance/chart/{symbol}", - {"range": "5d", "interval": "1d"}, - headers={"User-Agent": "Mozilla/5.0"}, - ) - result = raw["chart"]["result"][0] - closes = [c for c in result["indicators"]["quote"][0]["close"] if c is not None] - if len(closes) < 2: - return None - return {"last": closes[-1], "change_pct": (closes[-1] - closes[-2]) / closes[-2]} - except Exception: - return None - - -def fetch_macro_regime(): - """DXY (dolar endeksi), ABD 10 yıllık tahvil getirisi ve S&P 500'ün günlük yönünden basit - bir risk-on/risk-off rejim özeti çıkarır. Kripto gibi risk varlıkları genelde DXY ile ters, - SPX ile aynı yönde hareket eğilimindedir. Herhangi bir bileşen başarısız olursa o bileşen - atlanır; üçü de başarısız olursa katman tamamen devre dışı kalır.""" - def _load(): - dxy = _yahoo_last_change("DX-Y.NYB") - spx = _yahoo_last_change("%5EGSPC") - tnx = _yahoo_last_change("%5ETNX") - if not (dxy or spx or tnx): - return None - risk_on = risk_off = 0 - if dxy: - if dxy["change_pct"] > 0.002: - risk_off += 1 - elif dxy["change_pct"] < -0.002: - risk_on += 1 - if spx: - if spx["change_pct"] > 0.003: - risk_on += 1 - elif spx["change_pct"] < -0.003: - risk_off += 1 - if tnx: - if tnx["change_pct"] > 0.01: - risk_off += 1 - elif tnx["change_pct"] < -0.01: - risk_on += 1 - return {"dxy": dxy, "spx": spx, "tnx": tnx, "bias": risk_on - risk_off} - - return _cached(("macro",), MACRO_CACHE_TTL, _load) - - -# --------------------------------------------------------------------------- -# TECHNICAL INDICATORS -# --------------------------------------------------------------------------- -def ema(s, p): - return s.ewm(span=p, adjust=False).mean() - - -def sma(s, p): - return s.rolling(p).mean() - - -def rsi(s, p=14): - d = s.diff() - gain = d.clip(lower=0) - loss = -d.clip(upper=0) - ag = gain.ewm(alpha=1 / p, adjust=False).mean() - al = loss.ewm(alpha=1 / p, adjust=False).mean() - rs = ag / al.replace(0, np.nan) - return (100 - 100 / (1 + rs)).fillna(50) - - -def macd(s, fast=12, slow=26, signal=9): - line = ema(s, fast) - ema(s, slow) - sig = ema(line, signal) - return line, sig, line - sig - - -def bollinger(s, p=20, k=2): - mid = sma(s, p) - std = s.rolling(p).std() - return mid + k * std, mid, mid - k * std - - -def atr(df, p=14): - h, l, c = df["h"], df["l"], df["c"] - pc = c.shift(1) - tr = pd.concat([h - l, (h - pc).abs(), (l - pc).abs()], axis=1).max(axis=1) - return tr.ewm(alpha=1 / p, adjust=False).mean() - - -def stochastic(df, k_p=14, d_p=3): - lo = df["l"].rolling(k_p).min() - hi = df["h"].rolling(k_p).max() - k = 100 * (df["c"] - lo) / (hi - lo).replace(0, np.nan) - d = k.rolling(d_p).mean() - return k.fillna(50), d.fillna(50) - - -def vwap_window(df): - tp = (df["h"] + df["l"] + df["c"]) / 3 - return (tp * df["vol"]).cumsum() / df["vol"].cumsum().replace(0, np.nan) - - -def adx(df, p=14): - """ADX + yönsel indikatörler (+DI / -DI). Trend gücünü ölçer (0-100).""" - h, l, c = df["h"], df["l"], df["c"] - up_move = h.diff() - down_move = -l.diff() - plus_dm = np.where((up_move > down_move) & (up_move > 0), up_move, 0.0) - minus_dm = np.where((down_move > up_move) & (down_move > 0), down_move, 0.0) - tr = pd.concat([h - l, (h - c.shift(1)).abs(), (l - c.shift(1)).abs()], axis=1).max(axis=1) - atr_ = tr.ewm(alpha=1 / p, adjust=False).mean() - plus_di = 100 * pd.Series(plus_dm, index=df.index).ewm(alpha=1 / p, adjust=False).mean() / atr_.replace(0, np.nan) - minus_di = 100 * pd.Series(minus_dm, index=df.index).ewm(alpha=1 / p, adjust=False).mean() / atr_.replace(0, np.nan) - dx = ((plus_di - minus_di).abs() / (plus_di + minus_di).replace(0, np.nan)) * 100 - adx_ = dx.ewm(alpha=1 / p, adjust=False).mean() - return adx_.fillna(0), plus_di.fillna(0), minus_di.fillna(0) - - -def obv(df): - """On-Balance Volume: hacmin fiyat yönüyle kümülatif toplamı.""" - direction = np.sign(df["c"].diff()).fillna(0) - return (direction * df["vol"]).cumsum() - - -def supertrend(df, period=10, mult=3.0): - """Supertrend: ATR bantlı trend takipçisi. dir=+1 yükseliş, -1 düşüş. (nedensel, geleceğe bakmaz)""" - h, l, c = df["h"].to_numpy(), df["l"].to_numpy(), df["c"].to_numpy() - a = atr(df, period).to_numpy() - hl2 = (h + l) / 2 - ub, lb = hl2 + mult * a, hl2 - mult * a - n = len(c) - fub, flb = ub.copy(), lb.copy() - direction = np.ones(n) - line = np.full(n, np.nan) - if n: - line[0] = flb[0] - for i in range(1, n): - fub[i] = ub[i] if (ub[i] < fub[i - 1] or c[i - 1] > fub[i - 1]) else fub[i - 1] - flb[i] = lb[i] if (lb[i] > flb[i - 1] or c[i - 1] < flb[i - 1]) else flb[i - 1] - if c[i] > fub[i - 1]: - direction[i] = 1 - elif c[i] < flb[i - 1]: - direction[i] = -1 - else: - direction[i] = direction[i - 1] - line[i] = flb[i] if direction[i] == 1 else fub[i] - return pd.Series(line, index=df.index), pd.Series(direction, index=df.index) - - -def ichimoku(df): - """Tenkan/Kijun + bulut. Bulut, 26 bar önce hesaplanan Senkou A/B'nin bugüne kaydırılmış halidir - (yani nedensel: gelecek veri kullanılmaz).""" - h, l = df["h"], df["l"] - tenkan = (h.rolling(9).max() + l.rolling(9).min()) / 2 - kijun = (h.rolling(26).max() + l.rolling(26).min()) / 2 - span_a = (tenkan + kijun) / 2 - span_b = (h.rolling(52).max() + l.rolling(52).min()) / 2 - return tenkan, kijun, span_a.shift(26), span_b.shift(26) - - -def mfi(df, p=14): - """Money Flow Index: hacim ağırlıklı RSI.""" - tp = (df["h"] + df["l"] + df["c"]) / 3 - rmf = tp * df["vol"] - d = tp.diff() - pos = rmf.where(d > 0, 0.0).rolling(p).sum() - neg = rmf.where(d < 0, 0.0).rolling(p).sum() - out = 100 - 100 / (1 + pos / neg.replace(0, np.nan)) - out = out.where(~((neg == 0) & (pos > 0)), 100.0) - return out.fillna(50) - - -def cmf(df, p=20): - """Chaikin Money Flow: kapanışın bar aralığındaki konumuna göre hacim akışı (-1..+1).""" - rng = (df["h"] - df["l"]).replace(0, np.nan) - mfm = ((df["c"] - df["l"]) - (df["h"] - df["c"])) / rng - mfv = mfm.fillna(0) * df["vol"] - return (mfv.rolling(p).sum() / df["vol"].rolling(p).sum().replace(0, np.nan)).fillna(0) - - -def market_structure(df, w=10): - """+1: Higher-High & Higher-Low (yükseliş yapısı), -1: Lower-High & Lower-Low, 0: karışık.""" - hi, lo = df["h"].rolling(w).max(), df["l"].rolling(w).min() - up = (hi > hi.shift(w)) & (lo > lo.shift(w)) - dn = (hi < hi.shift(w)) & (lo < lo.shift(w)) - return pd.Series(np.where(up, 1.0, np.where(dn, -1.0, 0.0)), index=df.index) - - -def squeeze_indicator(df, ind): - """TTM tarzı sıkışma: Bollinger bantları Keltner kanalının İÇİNE girerse volatilite sıkışmıştır. - Dönüş: (sıkışma_açık, son 3 barda sıkışma çözüldü mü, momentum)""" - kc_mid = ind["ema20"] - kc_u, kc_l = kc_mid + 1.5 * ind["atr"], kc_mid - 1.5 * ind["atr"] - on = (ind["bb_lower"] > kc_l) & (ind["bb_upper"] < kc_u) - release = (~on) & on.shift(1, fill_value=False) - rel3 = release.astype(float).rolling(3, min_periods=1).max() - hh, ll = df["h"].rolling(20).max(), df["l"].rolling(20).min() - mom = df["c"] - ((hh + ll) / 2 + sma(df["c"], 20)) / 2 - return on.astype(float), rel3, mom - - -CANDLE_PATTERNS = [ - ("pat_hammer", "Çekiç", 1), - ("pat_bull_eng", "Yükseliş Yutan Mum", 1), - ("pat_morning", "Sabah Yıldızı", 1), - ("pat_shooting", "Kayan Yıldız", -1), - ("pat_bear_eng", "Düşüş Yutan Mum", -1), - ("pat_evening", "Akşam Yıldızı", -1), -] - - -def candle_patterns(df): - """Vektörel mum formasyonu tespiti (yalnızca geçmiş barlar kullanılır).""" - o, h, l, c = df["o"], df["h"], df["l"], df["c"] - body = (c - o).abs() - rng = (h - l).replace(0, np.nan) - upper = h - np.maximum(o, c) - lower = np.minimum(o, c) - l - avg_body = body.rolling(10, min_periods=3).mean() - prior_down = c.shift(1) < c.shift(4) - prior_up = c.shift(1) > c.shift(4) - - hammer = (lower >= 0.6 * rng) & (body <= 0.3 * rng) & (upper <= 0.15 * rng) & prior_down - shooting = (upper >= 0.6 * rng) & (body <= 0.3 * rng) & (lower <= 0.15 * rng) & prior_up - bull_eng = ((c.shift(1) < o.shift(1)) & (c > o) & (c >= o.shift(1)) & (o <= c.shift(1)) - & (body > body.shift(1)) & prior_down) - bear_eng = ((c.shift(1) > o.shift(1)) & (c < o) & (c <= o.shift(1)) & (o >= c.shift(1)) - & (body > body.shift(1)) & prior_up) - big2 = body.shift(2) > avg_body.shift(2) - small1 = body.shift(1) < 0.35 * body.shift(2) - mid2 = (o.shift(2) + c.shift(2)) / 2 - morning = (c.shift(2) < o.shift(2)) & big2 & small1 & (c > o) & (c > mid2) - evening = (c.shift(2) > o.shift(2)) & big2 & small1 & (c < o) & (c < mid2) - - raw = {"pat_hammer": hammer, "pat_shooting": shooting, "pat_bull_eng": bull_eng, - "pat_bear_eng": bear_eng, "pat_morning": morning, "pat_evening": evening} - return {k: v.fillna(False).astype(float) for k, v in raw.items()} - - -def find_swings(series, window=3): - """Yerel tepe/dip (swing high/low) indekslerini bulur.""" - n = len(series) - highs, lows = [], [] - for i in range(window, n - window): - seg = series.iloc[i - window:i + window + 1] - if series.iloc[i] == seg.max(): - highs.append(i) - if series.iloc[i] == seg.min(): - lows.append(i) - return highs, lows - - -def detect_divergence(df, ind, lookback=60, window=3): - """Fiyat ile RSI arasındaki son swing'leri karşılaştırarak klasik uyumsuzluğu tespit eder.""" - n = len(df) - lb = min(lookback, n) - close = df["c"].iloc[-lb:].reset_index(drop=True) - rsi_s = ind["rsi"].iloc[-lb:].reset_index(drop=True) - highs, lows = find_swings(close, window) - result = {"bullish": False, "bearish": False} - if len(highs) >= 2: - h1, h2 = highs[-2], highs[-1] - if close.iloc[h2] > close.iloc[h1] and rsi_s.iloc[h2] < rsi_s.iloc[h1]: - result["bearish"] = True - if len(lows) >= 2: - l1, l2 = lows[-2], lows[-1] - if close.iloc[l2] < close.iloc[l1] and rsi_s.iloc[l2] > rsi_s.iloc[l1]: - result["bullish"] = True - return result - - -def find_sr_levels(df, atr_v, window=5, lookback=200, max_levels=4): - """Swing tepe/diplerini kümeleyerek destek/direnç seviyeleri çıkarır. - Dönüş: (destekler, dirençler) — her biri [(seviye, temas_sayısı), ...], fiyata yakınlık sırasıyla.""" - sub = df.iloc[-lookback:].reset_index(drop=True) - highs, _ = find_swings(sub["h"], window) - _, lows = find_swings(sub["l"], window) - pts = [float(sub["h"].iloc[i]) for i in highs] + [float(sub["l"].iloc[i]) for i in lows] - if not pts: - return [], [] - price = float(df["c"].iloc[-1]) - tol = max(0.6 * (atr_v if atr_v and atr_v > 0 else 0.0), price * 0.002) - pts.sort() - clusters, cur = [], [pts[0]] - for p in pts[1:]: - if p - float(np.mean(cur)) <= tol: - cur.append(p) - else: - clusters.append(cur) - cur = [p] - clusters.append(cur) - levels = [(float(np.mean(c)), len(c)) for c in clusters] - supports = sorted([lv for lv in levels if lv[0] < price], key=lambda x: -x[0])[:max_levels] - resists = sorted([lv for lv in levels if lv[0] > price], key=lambda x: x[0])[:max_levels] - return supports, resists - - -def compute_liquidation_clusters(liqs, price, atr_v, n_bins=LIQ_CLUSTER_BINS, max_levels=LIQ_CLUSTER_MAX_LEVELS): - """Ham tasfiye kayıtlarını fiyat eksenine göre kümeler (bucket). - Her küme için toplam nominal büyüklük (yaklaşık USDT), long/short kırılımı, - baskın yön (bias: +1 long ağırlıklı, -1 short ağırlıklı) ve fiyata ATR - cinsinden uzaklık hesaplanır. En büyük `max_levels` küme, büyükten küçüğe - döner. side='sell' -> zorla satılan pozisyon (long tasfiyesi); - side='buy' -> zorla alınan pozisyon (short tasfiyesi).""" - if not liqs or not atr_v or atr_v <= 0: - return [] - prices = [l["px"] for l in liqs] - lo, hi = min(prices + [price]), max(prices + [price]) - if hi <= lo: - return [] - width = (hi - lo) / max(n_bins, 1) - if width <= 0: - return [] - buckets = {} - for l in liqs: - idx = min(n_bins - 1, max(0, int((l["px"] - lo) / width))) - b = buckets.setdefault(idx, {"long_notional": 0.0, "short_notional": 0.0, "px_sum": 0.0, "n": 0}) - notional = l["px"] * l["sz"] - if l["side"] == "sell": - b["long_notional"] += notional - else: - b["short_notional"] += notional - b["px_sum"] += l["px"] - b["n"] += 1 - clusters = [] - for b in buckets.values(): - total = b["long_notional"] + b["short_notional"] - if total < LIQ_MIN_TOTAL_NOTIONAL: - continue - mid = b["px_sum"] / b["n"] - clusters.append({ - "price": mid, "notional": total, - "long_notional": b["long_notional"], "short_notional": b["short_notional"], - "bias": (b["long_notional"] - b["short_notional"]) / total, - "dist_atr": abs(mid - price) / atr_v, - "n": b["n"], - }) - clusters.sort(key=lambda c: c["notional"], reverse=True) - return clusters[:max_levels] - - -def compute_indicators(df): - close = df["c"] - ind = {} - ind["rsi"] = rsi(close) - ind["macd_line"], ind["macd_signal"], ind["macd_hist"] = macd(close) - ind["ema20"] = ema(close, 20) - ind["ema50"] = ema(close, 50) - ind["ema200"] = ema(close, 200 if len(close) >= 200 else max(5, len(close) // 2)) - ind["bb_upper"], ind["bb_mid"], ind["bb_lower"] = bollinger(close) - ind["atr"] = atr(df) - ind["stoch_k"], ind["stoch_d"] = stochastic(df) - ind["vwap"] = vwap_window(df) - ind["adx"], ind["plus_di"], ind["minus_di"] = adx(df) - ind["obv"] = obv(df) - ind["obv_ema"] = ema(ind["obv"], 20) - # --- v2 ek faktörler --- - ind["st_line"], ind["st_dir"] = supertrend(df) - ind["tenkan"], ind["kijun"], ind["ich_a"], ind["ich_b"] = ichimoku(df) - ind["mfi"] = mfi(df) - ind["cmf"] = cmf(df) - ind["rvol"] = (df["vol"] / sma(df["vol"], 20).replace(0, np.nan)).fillna(1.0) - ind["dc_hi20"] = df["h"].rolling(20).max().shift(1) - ind["dc_lo20"] = df["l"].rolling(20).min().shift(1) - ind["struct"] = market_structure(df) - ind["sq_on"], ind["sq_rel3"], ind["sq_mom"] = squeeze_indicator(df, ind) - ind["atr_rank"] = (ind["atr"] / close).rolling(100, min_periods=30).apply( - lambda x: float((x <= x[-1]).mean()), raw=True).fillna(0.5) - for name, ser in candle_patterns(df).items(): - ind[name] = ser - # hızlı, nedensel erişim için numpy önbelleği (backtest döngüsünde .iloc yerine kullanılır) - arr = {k: v.to_numpy(dtype=float) for k, v in ind.items()} - for k in ("o", "h", "l", "c", "vol"): - arr[k] = df[k].to_numpy(dtype=float) - ind["_arr"] = arr - return ind - - -# --------------------------------------------------------------------------- -# COMPOSITE SIGNAL ENGINE v2 (multi-factor, rejim-uyarlamalı, konsensüs filtreli) -# --------------------------------------------------------------------------- -def _score_at(ind, i, verbose=False): - """Çoklu faktör puanlamasını verilen bar indeksinde hesaplar (yalnızca o ana kadarki veri). - - İki faktör ailesi vardır: - T (TREND) : MACD, EMA dizilimi, VWAP, OBV, Supertrend, Ichimoku, Donchian kırılımı, - piyasa yapısı, CMF, sıkışma kırılımı, ADX/DI yönü - R (REVERSION) : RSI, Bollinger, Stokastik, MFI, mum formasyonları - - ADX'e göre iki ailenin ağırlığı SÜREKLİ olarak kayar (w: 0=yatay … 1=güçlü trend). - Güçlü trendde karşı-trend (aşırı alım/satım) sinyalleri bastırılır; yatay piyasada trend takibi bastırılır. - Sonuç, tanh ile ±10 aralığına yumuşak sıkıştırılır. - Canlı sinyal ve tarihsel backtest AYNI fonksiyonu kullanır (tutarlılık + gelecek sızıntısı yok).""" - A = ind["_arr"] - price = A["c"][i] - items = [] # (etiket, grup, ham_puan, açıklama) - - def add(label, group, raw, text): - items.append((label, group, float(raw), text)) - - # --- RSI (R) --- - rsi_v = A["rsi"][i] - if rsi_v < 30: - add("RSI", "R", 2, f"RSI aşırı satım bölgesinde ({rsi_v:.1f})") - elif rsi_v > 70: - add("RSI", "R", -2, f"RSI aşırı alım bölgesinde ({rsi_v:.1f})") - else: - add("RSI", "R", 0, f"RSI nötr bölgede ({rsi_v:.1f})") - - # --- MACD histogramı (T) --- - hist_v = A["macd_hist"][i] - hist_prev = A["macd_hist"][i - 1] if i > 0 else 0.0 - if hist_v > 0 and hist_v > hist_prev: - add("MACD", "T", 1.5, "MACD histogramı pozitif ve güçleniyor") - elif hist_v > 0: - add("MACD", "T", 0.5, "MACD histogramı pozitif") - elif hist_v < 0 and hist_v < hist_prev: - add("MACD", "T", -1.5, "MACD histogramı negatif ve zayıflıyor") - else: - add("MACD", "T", -0.5, "MACD histogramı negatif") - - # --- EMA dizilimi (T) --- - e20, e50, e200 = A["ema20"][i], A["ema50"][i], A["ema200"][i] - if price > e20 > e50 > e200: - add("EMA Dizilimi", "T", 2, "Güçlü yükseliş trendi (Fiyat > EMA20 > EMA50 > EMA200)") - elif price > e20 and e20 > e50: - add("EMA Dizilimi", "T", 1, "Yükseliş trendi (kısa vadeli ortalamaların üzerinde)") - elif price < e20 < e50 < e200: - add("EMA Dizilimi", "T", -2, "Güçlü düşüş trendi (Fiyat < EMA20 < EMA50 < EMA200)") - elif price < e20 and e20 < e50: - add("EMA Dizilimi", "T", -1, "Düşüş trendi (kısa vadeli ortalamaların altında)") - else: - add("EMA Dizilimi", "T", 0, "Trend yatay / belirsiz") - - # --- Bollinger konumu (R) --- - bb_u, bb_l = A["bb_upper"][i], A["bb_lower"][i] - if bb_u == bb_u and bb_l == bb_l and bb_u != bb_l: - bb_pos = (price - bb_l) / (bb_u - bb_l) - if bb_pos < 0.15: - add("Bollinger", "R", 1, "Fiyat Bollinger alt bandına yakın (olası tepki)") - elif bb_pos > 0.85: - add("Bollinger", "R", -1, "Fiyat Bollinger üst bandına yakın (olası tepki)") - - # --- Stokastik (R) --- - k_v = A["stoch_k"][i] - if k_v < 20: - add("Stokastik", "R", 1, f"Stokastik aşırı satım bölgesinde ({k_v:.1f})") - elif k_v > 80: - add("Stokastik", "R", -1, f"Stokastik aşırı alım bölgesinde ({k_v:.1f})") - - # --- MFI (R) --- - mfi_v = A["mfi"][i] - if mfi_v < 20: - add("MFI", "R", 1, f"MFI (hacimli RSI) aşırı satımda ({mfi_v:.1f})") - elif mfi_v > 80: - add("MFI", "R", -1, f"MFI (hacimli RSI) aşırı alımda ({mfi_v:.1f})") - - # --- Mum formasyonları (R) --- - hits = [] - for key, name, sgn in CANDLE_PATTERNS: - if A[key][i] > 0: - hits.append((name, sgn)) - if hits: - raw = max(-1.5, min(1.5, float(sum(s for _, s in hits)))) - add("Mum Formasyonu", "R", raw, "Mum formasyonu: " + ", ".join(n for n, _ in hits)) - - # --- VWAP (T) --- - vwap_v = A["vwap"][i] - if vwap_v == vwap_v: - if price > vwap_v: - add("VWAP", "T", 0.5, "Fiyat pencere VWAP'ının üzerinde") - else: - add("VWAP", "T", -0.5, "Fiyat pencere VWAP'ının altında") - - # --- OBV (T) --- - obv_v, obv_e = A["obv"][i], A["obv_ema"][i] - if obv_e == obv_e: - if obv_v > obv_e: - add("OBV", "T", 0.5, "OBV hacim akışı yükseliş yönünde") - else: - add("OBV", "T", -0.5, "OBV hacim akışı düşüş yönünde") - - # --- CMF (T) --- - cmf_v = A["cmf"][i] - if cmf_v > 0.05: - add("CMF", "T", 0.5, f"Chaikin para akışı pozitif ({cmf_v:+.2f}) — alıcılar baskın") - elif cmf_v < -0.05: - add("CMF", "T", -0.5, f"Chaikin para akışı negatif ({cmf_v:+.2f}) — satıcılar baskın") - - # --- Supertrend (T) --- - st_d = A["st_dir"][i] - st_flip = i > 0 and A["st_dir"][i] != A["st_dir"][i - 1] - if st_d > 0: - add("Supertrend", "T", 1.5 + (0.5 if st_flip else 0), - "Supertrend YÜKSELİŞ moduna döndü (taze sinyal)" if st_flip else "Supertrend yükseliş modunda") - else: - add("Supertrend", "T", -(1.5 + (0.5 if st_flip else 0)), - "Supertrend DÜŞÜŞ moduna döndü (taze sinyal)" if st_flip else "Supertrend düşüş modunda") - - # --- Ichimoku (T) --- - sa, sb = A["ich_a"][i], A["ich_b"][i] - if sa == sa and sb == sb: - top, bot = max(sa, sb), min(sa, sb) - if price > top: - add("Ichimoku Bulutu", "T", 1.5, "Fiyat Ichimoku bulutunun üzerinde (boğa bölgesi)") - elif price < bot: - add("Ichimoku Bulutu", "T", -1.5, "Fiyat Ichimoku bulutunun altında (ayı bölgesi)") - else: - add("Ichimoku Bulutu", "T", 0, "Fiyat Ichimoku bulutunun içinde (kararsız bölge)") - tk, kj = A["tenkan"][i], A["kijun"][i] - if tk == tk and kj == kj: - add("Tenkan/Kijun", "T", 0.5 if tk > kj else -0.5, - "Tenkan > Kijun (kısa vadeli momentum yukarı)" if tk > kj else "Tenkan < Kijun (kısa vadeli momentum aşağı)") - - # --- Donchian kırılımı (T) --- - dhi, dlo, rvol = A["dc_hi20"][i], A["dc_lo20"][i], A["rvol"][i] - if dhi == dhi and dlo == dlo: - if price > dhi: - if rvol >= 1.2: - add("Donchian Kırılımı", "T", 1.5, f"20 barlık tepe HACİMLİ kırıldı (göreli hacim {rvol:.1f}x)") - else: - add("Donchian Kırılımı", "T", 0.75, f"20 barlık tepe kırıldı ama hacim zayıf ({rvol:.1f}x)") - elif price < dlo: - if rvol >= 1.2: - add("Donchian Kırılımı", "T", -1.5, f"20 barlık dip HACİMLİ kırıldı (göreli hacim {rvol:.1f}x)") - else: - add("Donchian Kırılımı", "T", -0.75, f"20 barlık dip kırıldı ama hacim zayıf ({rvol:.1f}x)") - - # --- Piyasa yapısı (T) --- - st_v = A["struct"][i] - if st_v > 0: - add("Piyasa Yapısı", "T", 1, "Piyasa yapısı: Higher-High / Higher-Low (yükseliş yapısı)") - elif st_v < 0: - add("Piyasa Yapısı", "T", -1, "Piyasa yapısı: Lower-High / Lower-Low (düşüş yapısı)") - - # --- Sıkışma (Bollinger ⊂ Keltner) --- - squeeze_on = A["sq_on"][i] > 0 - mom = A["sq_mom"][i] - if A["sq_rel3"][i] > 0 and mom == mom and mom != 0: - d = 1 if mom > 0 else -1 - add("Sıkışma Kırılımı", "T", 1.5 * d, - "Volatilite sıkışması yukarı yönde çözüldü" if d > 0 else "Volatilite sıkışması aşağı yönde çözüldü") - elif squeeze_on: - add("Sıkışma", "T", 0, "Volatilite sıkışması aktif — kırılım bekleniyor, sinyal güveni düşürüldü") - - # --- REJİM (ADX) — sürekli ağırlıklandırma --- - adx_v = A["adx"][i] - w = min(max((adx_v - 15.0) / 15.0, 0.0), 1.0) if adx_v == adx_v else 0.0 # 0 = yatay, 1 = güçlü trend - trend_w = 0.5 + 0.8 * w - rev_w = 1.3 - 0.95 * w - di_dir = 1 if A["plus_di"][i] > A["minus_di"][i] else -1 - add("ADX / DI Yönü", "T", di_dir * w, "") # yönsel bonus; w kadar ölçeklenir - if w >= 0.67: - regime = "TREND" - add("Rejim", "T", 0, f"ADX güçlü trend gösteriyor ({adx_v:.1f}) — trend faktörleri ağırlıklı, karşı-trend sinyaller bastırıldı") - elif w <= 0.33: - regime = "YATAY" - add("Rejim", "T", 0, f"ADX zayıf/yatay piyasa ({adx_v:.1f}) — ortalamaya dönüş faktörleri ağırlıklı") - else: - regime = "GEÇİŞ" - add("Rejim", "T", 0, f"ADX geçiş bölgesinde ({adx_v:.1f}) — faktör ağırlıkları dengeli karışım") - - contribs = [(lab, (trend_w if g == "T" else rev_w) * raw, raw) for lab, g, raw, _ in items] - raw_total = sum(c for _, c, _ in contribs) - if squeeze_on: - raw_total *= 0.75 - score = 10.0 * math.tanh(raw_total / 9.0) - bull_w = sum(c for _, c, _ in contribs if c > 0) - bear_w = -sum(c for _, c, _ in contribs if c < 0) - - out = { - "score": score, "price": price, "adx": adx_v, "w": w, "regime": regime, - "bull_w": bull_w, "bear_w": bear_w, "squeeze_on": bool(squeeze_on), - "atr_rank": A["atr_rank"][i], "pat_hits": hits, - } - if verbose: - def tone(raw): - return "up" if raw > 0 else ("down" if raw < 0 else "flat") - out["reasons"] = [(tone(raw), text) for _, _, raw, text in items if text] - out["contribs"] = [(lab, c) for lab, c, raw in contribs if abs(c) > 1e-9] - return out - - -def compute_score_series(ind, n): - """Son BACKTEST_LOOKBACK bar için (canlı bar dahil) skor serisi. Dönüş: (başlangıç_indeksi, [skor_dict,...])""" - start = min(max(WARMUP_BARS, n - BACKTEST_LOOKBACK), n - 1) - return start, [_score_at(ind, i) for i in range(start, n)] - - -# --------------------------------------------------------------------------- -# BACKTEST — yön doğruluğu + gerçek işlem simülasyonu (SL/TP/zaman çıkışı + komisyon) -# --------------------------------------------------------------------------- -def _factor_gate_ok(s): - """v3 sert filtre: bir skor barının 'kaliteli sinyal' sayılıp sayılmayacağını belirler. - Hem CANLI sinyalde hem BACKTEST'te AYNI fonksiyon kullanılır (tutarlılık, iyimser backtest yok). - İki koşul: (1) belirgin bir rejim var mı (ADX / sıkışma), (2) faktörler gerçekten aynı yönde mi - (konsensüs). Bu ikisi zayıfsa bar, isabet istatistiğine hiç dahil edilmez / işlem açılmaz.""" - adx_v = s.get("adx") - if adx_v == adx_v and adx_v < MIN_REGIME_ADX and not s.get("squeeze_on"): - return False - tot = s["bull_w"] + s["bear_w"] - if tot <= 0: - return True - direction = 1 if s["score"] > 0 else -1 - agree = (s["bull_w"] if direction > 0 else s["bear_w"]) / tot - return agree >= MIN_AGREE_FOR_SIGNAL - - -def directional_accuracy(df, start, scores, forward=5, threshold=SIGNAL_THRESHOLD): - """Klasik test: sinyal 'forward' bar sonra doğru yönü göstermiş mi? (rejim kırılımlı, - v3 sert filtresinden geçen barlar üzerinden). Ayrıca ilk-yarı / ikinci-yarı isabet oranını - karşılaştırarak rejim kayması ya da parametre aşırı-uyumu (overfit) belirtisi olup olmadığını işaretler.""" - c = df["c"].to_numpy() - n = len(c) - stats = {"all": [0, 0], "trend": [0, 0], "range": [0, 0]} - signal_hits = [] - for i in range(start, n - forward): - s = scores[i - start] - if abs(s["score"]) < threshold or not _factor_gate_ok(s): - continue - correct = (s["score"] > 0) == (c[i + forward] > c[i]) - regime = "trend" if s["adx"] >= 25 else "range" - for key in ("all", regime): - stats[key][1] += 1 - if correct: - stats[key][0] += 1 - signal_hits.append(correct) - 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 - - half1_acc = half2_acc = None - stable = True - if len(signal_hits) >= STABILITY_MIN_SAMPLES: - mid = len(signal_hits) // 2 - h1, h2 = signal_hits[:mid], signal_hits[mid:] - half1_acc = sum(h1) / len(h1) - half2_acc = sum(h2) / len(h2) - stable = abs(half1_acc - half2_acc) <= STABILITY_MAX_GAP - - return { - "accuracy": stats["all"][0] / stats["all"][1], "correct": stats["all"][0], - "n_signals": stats["all"][1], "forward_bars": forward, - "trend_regime": pack("trend"), "range_regime": pack("range"), - "half1_acc": half1_acc, "half2_acc": half2_acc, "stable": stable, - } - - -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: - sd = scores[i - start] - s = sd["score"] - a = atr_a[i] - if abs(s) < threshold or not (a > 0) or not _factor_gate_ok(sd): - 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, symbol=None, bar=None): - n = len(df) - if n - start < 20: - return None - - # --- FinRL + SAC (v9): SAC ajanının hedef pozisyon önerisi backtest skorlarına eklenir --- - # (bkz. compute_sac_backtest_deltas docstring'i — leakage sınırlaması burada geçerlidir) - sac_deltas, sac_n = compute_sac_backtest_deltas(ind, n, start, symbol, bar) - if sac_deltas is not None: - scores = [ - {**s, "score": max(-10.0, min(10.0, s["score"] + d))} - for s, d in zip(scores, sac_deltas) - ] - - 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, - "sac_applied": sac_deltas is not None, "sac_n_samples": sac_n} - if summ: - bt["stats"], bt["equity"], bt["bh"] = summ - return bt - - -# --------------------------------------------------------------------------- -# SİNİR AĞI (v4) — hafif, tek-gizli-katmanlı MLP, sadece NumPy (harici DL bağımlılığı yok) -# --------------------------------------------------------------------------- -# Neden torch/tensorflow değil: bu bir Gradio/HF Spaces panelidir; ağır bir DL kütüphanesi -# başlangıç süresini ve dağıtım boyutunu ciddi artırır. Özellik sayısı azdır (13), örneklem -# küçüktür (~150-250 bar) — bu ölçekte elle yazılmış küçük bir MLP, pratikte ağır kütüphanelerle -# aynı işi görür ve tamamen şeffaftır (ne öğrendiği kolayca incelenebilir). -# -# SIZINTI KORUMASI: NN, MTF/emir defteri/funding gibi bir "canlı katman"dır — backtest'in -# kullandığı `scores` serisine hiç girmez, sadece generate_signal() içindeki CANLI skoru etkiler. -# Bu yüzden tek gerçek kural şudur: model, tahmin ettiği canlı barı KENDİ eğitim etiketlerinde -# görmez (son NN_FORWARD bar eğitimden hariç tutulur). Model kendi eğitim setinin son %20'si -# üzerinde kaba bir in-sample doğruluk raporlar (gerçek bir holdout değildir — sadece "bu model -# saçmalıyor mu" kontrolü; asıl doğrulama her zaman directional_accuracy'nin backtest'idir). -NN_FEATURE_NAMES = [ - "RSI", "MACD Hist", "ADX", "+DI/-DI Farkı", "Stokastik", "MFI", "CMF", - "Göreli Hacim", "ATR Yüzdeliği", "Piyasa Yapısı", "Sıkışma", "Bollinger Konumu", "EMA20>EMA50", -] - - -def _nn_build_features(ind, idx): - """idx barındaki (yalnızca o ana kadarki veriyle, nedensel) normalize edilmiş öznitelik vektörü.""" - A = ind["_arr"] - price = A["c"][idx] - e20, e50 = A["ema20"][idx], A["ema50"][idx] - bb_u, bb_l = A["bb_upper"][idx], A["bb_lower"][idx] - bb_pos = (price - bb_l) / (bb_u - bb_l) if (bb_u == bb_u and bb_u != bb_l) else 0.5 - return np.array([ - (A["rsi"][idx] - 50.0) / 50.0, - math.tanh(A["macd_hist"][idx] / (abs(price) * 0.002 + 1e-9)), - min(A["adx"][idx], 60.0) / 60.0 if A["adx"][idx] == A["adx"][idx] else 0.0, - (A["plus_di"][idx] - A["minus_di"][idx]) / 100.0, - (A["stoch_k"][idx] - 50.0) / 50.0, - (A["mfi"][idx] - 50.0) / 50.0, - max(-1.0, min(1.0, A["cmf"][idx])), - math.tanh(A["rvol"][idx] - 1.0), - (A["atr_rank"][idx] - 0.5) * 2.0, - float(A["struct"][idx]), - 1.0 if A["sq_on"][idx] > 0 else 0.0, - (bb_pos - 0.5) * 2.0, - 1.0 if e20 > e50 else -1.0, - ], dtype=float) - - -def _nn_dataset(ind, n, forward=NN_FORWARD): - """Eğitim seti: WARMUP_BARS'tan sonraki TÜM barlar, son 'forward' bar hariç (onların - gelecek etiketi henüz bilinmiyor). NN, backtest edilen `scores` serisine hiç girmiyor — - MTF/emir defteri/funding gibi SADECE canlı sinyele eklenen bir katman; bu yüzden burada - `start` (backtest penceresi) sınırına uymak gereksizdi ve veriyi habersizce 0'a düşürüyordu. - Tek gerçek kural: canlı tahmin yapılan son bar, kendi eğitim setinde ETİKET olarak yer almaz.""" - A = ind["_arr"] - c = A["c"] - lo = max(WARMUP_BARS, 30) - hi = n - forward # son 'forward' bar hariç -> tahmin edilen canlı bar hiçbir zaman eğitime sızmaz - if hi - lo < NN_MIN_TRAIN: - return None, None - X = np.stack([_nn_build_features(ind, i) for i in range(lo, hi)]) - y = np.array([1.0 if c[i + forward] > c[i] else 0.0 for i in range(lo, hi)]) - return X, y - - -class _TinyMLP: - """Elle yazılmış, tek gizli katmanlı (ReLU) + sigmoid çıkışlı ileri-beslemeli sinir ağı. - Tam-batch gradient descent ile eğitilir; harici bir DL kütüphanesi gerektirmez.""" - - def __init__(self, n_in, n_hidden=NN_HIDDEN, seed=42): - rng = np.random.default_rng(seed) - self.w1 = rng.normal(0, math.sqrt(2.0 / n_in), (n_in, n_hidden)) - self.b1 = np.zeros(n_hidden) - self.w2 = rng.normal(0, math.sqrt(2.0 / n_hidden), (n_hidden, 1)) - self.b2 = np.zeros(1) - self.mu = np.zeros(n_in) - self.sigma = np.ones(n_in) - - def _norm(self, X): - return (X - self.mu) / self.sigma - - def fit(self, X, y, epochs=NN_EPOCHS, lr=NN_LR, l2=NN_L2): - self.mu = X.mean(axis=0) - self.sigma = X.std(axis=0) + 1e-8 - Xn = self._norm(X) - yy = y.reshape(-1, 1) - n = len(Xn) - for _ in range(epochs): - z1 = Xn @ self.w1 + self.b1 - a1 = np.maximum(z1, 0.0) # ReLU - z2 = a1 @ self.w2 + self.b2 - p = 1.0 / (1.0 + np.exp(-z2)) # sigmoid - dz2 = (p - yy) / n - dw2 = a1.T @ dz2 + l2 * self.w2 - db2 = dz2.sum(axis=0) - da1 = dz2 @ self.w2.T - dz1 = da1 * (z1 > 0) - dw1 = Xn.T @ dz1 + l2 * self.w1 - db1 = dz1.sum(axis=0) - self.w1 -= lr * dw1 - self.b1 -= lr * db1 - self.w2 -= lr * dw2 - self.b2 -= lr * db2 - return self - - def predict_proba(self, X): - Xn = self._norm(np.atleast_2d(X)) - a1 = np.maximum(Xn @ self.w1 + self.b1, 0.0) - p = 1.0 / (1.0 + np.exp(-(a1 @ self.w2 + self.b2))) - return p.ravel() - - def feature_importance(self): - """Kaba bir önem ölçüsü: her girişin ilk katman ağırlıklarındaki mutlak etkisi (yorumlanabilirlik için).""" - raw = np.abs(self.w1).sum(axis=1) - tot = raw.sum() - return (raw / tot) if tot > 0 else raw - - -def get_nn_model(ind, n, symbol=None, bar=None): - """(sembol, bar, örneklem büyüklüğü) başına eğitilmiş modeli önbellekten döndürür; yoksa eğitir.""" - def _train(): - X, y = _nn_dataset(ind, n) - if X is None: - return None - model = _TinyMLP(X.shape[1]).fit(X, y) - k = max(10, int(len(X) * 0.2)) - pred = (model.predict_proba(X[-k:]) > 0.5).astype(float) - return { - "model": model, "train_acc": float((pred == y[-k:]).mean()), - "n_samples": len(X), "base_rate": float(y.mean()), - } - key = ("nn_model", symbol, bar, n) - try: - return _cached(key, NN_CACHE_TTL, _train) - except Exception: - return None - - -def get_lgbm_model(ind, n, symbol=None, bar=None): - """(sembol, bar, örneklem büyüklüğü) başına LightGBM sınıflandırıcı eğitip önbellekten döndürür. - NN katmanıyla aynı öznitelik/etiket şeması kullanılır (_nn_build_features / forward-bar yön etiketi); - böylece iki model AYNI veriyle, farklı bir öğrenme algoritmasıyla (ağaç tabanlı vs. sinir ağı) eğitilip - kompozit skora bağımsız iki "öğrenilmiş" oy olarak katkı sağlar. LightGBM kurulu değilse veya yeterli - geçmiş yoksa None döner; sinyal motoru bu katman olmadan da normal çalışmaya devam eder.""" - if not _LGBM_AVAILABLE: - return None - - def _train(): - X, y = _nn_dataset(ind, n, forward=LGBM_FORWARD) - if X is None or len(X) < LGBM_MIN_TRAIN: - return None - model = lgb.LGBMClassifier(**LGBM_PARAMS) - model.fit(X, y) - k = max(10, int(len(X) * 0.2)) - pred = model.predict(X[-k:]) - importances = model.feature_importances_ - tot_imp = importances.sum() - return { - "model": model, "train_acc": float((pred == y[-k:]).mean()), - "n_samples": len(X), "base_rate": float(y.mean()), - "importance": (importances / tot_imp) if tot_imp > 0 else importances, - } - key = ("lgbm_model", symbol, bar, n) - try: - return _cached(key, LGBM_CACHE_TTL, _train) - except Exception: - return None - - -_SingleAssetTradingEnv = None -if _SAC_AVAILABLE: - # Sınıf tanımı 'gym.Env'i temel sınıf olarak referans aldığı için yalnızca gymnasium - # kuruluyken (yukarıdaki import bloğu başarılıysa) tanımlanır — aksi halde modül, - # eksik bir bağımlılık yüzünden import anında çökmez (diğer opsiyonel katmanlarla aynı ilke). - class _SingleAssetTradingEnv(gym.Env): - """FinRL'in tek-varlık işlem ortamı mantığına yakın, hafif özel bir Gymnasium ortamı. - - Her barda ajan SÜREKLİ bir aksiyon seçer: hedef pozisyon a ∈ [-1, +1] (-1 tam kısa, - 0 nötr, +1 tam uzun — kaldıraçsız, hesap büyüklüğünün oranı olarak). Ödül, o barın - gerçekleşen getirisi × pozisyon, eksi pozisyon DEĞİŞİMİ başına komisyon cezasıdır - (FinRL'deki işlem maliyeti kavramıyla aynı mantık). Gözlem, NN katmanıyla aynı - nedensel öznitelik vektörünün son SAC_WINDOW barlık penceresi + mevcut pozisyondur. - - Bu ortam yalnızca EĞİTİM/ÇIKARIM amaçlıdır: gerçek bir borsaya emir GÖNDERMEZ, yalnızca - geçmiş barlar arasında ileri-adım simülasyonu yapar (nedensel — her adımda yalnızca o ana - kadarki veriye bakan öznitelikler kullanılır).""" - - metadata = {"render_modes": []} - - def __init__(self, ind, lo, hi, window=SAC_WINDOW, fee=SAC_FEE): - super().__init__() - self.ind = ind - self.lo, self.hi = lo, hi # kullanılabilir bar aralığı: adımlar [lo+window-1, hi-1) içinde atılır - self.window = window - self.fee = fee - self.close = ind["_arr"]["c"] - n_feat = _nn_build_features(ind, lo).shape[0] - obs_dim = n_feat * window + 1 # + mevcut pozisyon - self.observation_space = gym_spaces.Box(-np.inf, np.inf, shape=(obs_dim,), dtype=np.float32) - self.action_space = gym_spaces.Box(-1.0, 1.0, shape=(1,), dtype=np.float32) - self._i = lo + window - 1 - self._pos = 0.0 - - def _obs(self): - feats = np.concatenate([_nn_build_features(self.ind, k) - for k in range(self._i - self.window + 1, self._i + 1)]) - return np.concatenate([feats, [self._pos]]).astype(np.float32) - - def reset(self, *, seed=None, options=None): - super().reset(seed=seed) - lo_i = self.lo + self.window - 1 - hi_i = max(lo_i + 1, self.hi - 2) # en az 1 adım atılabilecek şekilde - self._i = int(self.np_random.integers(lo_i, hi_i)) if hi_i > lo_i else lo_i - self._pos = 0.0 - return self._obs(), {} - - def step(self, action): - target_pos = float(np.clip(np.asarray(action).reshape(-1)[0], -1.0, 1.0)) - c0, c1 = self.close[self._i], self.close[self._i + 1] - ret = (c1 - c0) / c0 if c0 else 0.0 - reward = (target_pos * ret - self.fee * abs(target_pos - self._pos)) * SAC_REWARD_SCALE - self._pos = target_pos - self._i += 1 - terminated = self._i >= self.hi - 1 - truncated = False - return self._obs(), float(reward), terminated, truncated, {} - - -def get_sac_agent(ind, n, symbol=None, bar=None): - """(sembol, bar, örneklem büyüklüğü) başına, geçmiş barlar üzerinde OTOMATİK eğitilen bir - Soft Actor-Critic (stable-baselines3) ajanını önbellekten döndürür; yoksa sıfırdan eğitir. - FinRL/SAC kurulu değilse veya yeterli geçmiş yoksa None döner (diğer canlı katmanlarla aynı - 'sessizce atla' mantığı) — eğitim pahalı olduğu için NN/LightGBM'e göre daha uzun önbelleklenir - ve adım sayısı (SAC_TRAIN_TIMESTEPS) CPU'da makul sürede bitecek şekilde düşük tutulmuştur.""" - if not _SAC_AVAILABLE: - return None - - def _train(): - lo = max(WARMUP_BARS, 30) - hi = n - if hi - lo < SAC_MIN_TRAIN + SAC_WINDOW: - return None - env = _SingleAssetTradingEnv(ind, lo, hi, SAC_WINDOW, SAC_FEE) - model = SAC( - "MlpPolicy", env, policy_kwargs=dict(net_arch=[32, 32]), - learning_rate=3e-4, buffer_size=8000, batch_size=64, - learning_starts=300, train_freq=1, gradient_steps=1, verbose=0, - ) - model.learn(total_timesteps=SAC_TRAIN_TIMESTEPS, progress_bar=False) - return {"model": model, "lo": lo, "hi": hi, "n_samples": hi - lo} - key = ("sac_agent", symbol, bar, n) - try: - return _cached(key, SAC_CACHE_TTL, _train) - except Exception: - return None - - -def compute_sac_backtest_deltas(ind, n, start, symbol=None, bar=None): - """SAC ajanının backtest penceresi [start, n) için, HER geçmiş bar için nedensel bir gözlem - penceresiyle (o barın SAC_WINDOW kadar öncesine bakan öznitelikler + pozisyon=0 varsayımı, - canlı sinyaldeki obs inşasıyla AYNI) ürettiği hedef pozisyondan, canlı katmanla AYNI - (yön × güç × SAC_WEIGHT) formülüyle hesaplanan skor katkısını (delta) döndürür. - - Ajan burada YENİDEN EĞİTİLMEZ: get_sac_agent() çağrısı canlı sinyalle AYNI önbelleğe düşer - (aynı symbol/bar/n anahtarı), yani bu fonksiyon ekstra eğitim maliyeti getirmez. - - SINIRLAMA (leakage): get_sac_agent, [WARMUP_BARS, n) aralığındaki TÜM barlarla eğitilir — - bu aralık backtest penceresiyle [start, n) büyük ölçüde ÇAKIŞIR. Yani model, tam olarak test - edildiği barları (o barların gelecekteki getirilerini) eğitim sürecinde dolaylı olarak görmüş - olabilir; bu nedenle bu bir gerçek (eğitim dışı) walk-forward test DEĞİLDİR ve SAC'ın backtest - katkısı iyimser/optimist olabilir. Diğer canlı-öğrenen katmanlar (NN/LightGBM/Chronos) bu riski - taşımamak için backtest'e hiç dahil edilmiyor; SAC için açık kullanıcı talebi üzerine bu risk - kabul edilerek dahil edildi ve UI'da not olarak gösterilir. - - Dönüş: (deltas, n_samples) — deltas, len(range(start, n)) uzunluğunda float listesi; - ajan yoksa (kurulu değil / yeterli veri yok) (None, None) döner.""" - agent = get_sac_agent(ind, n, symbol, bar) - if not agent: - return None, None - idxs = list(range(start, n)) - obs_list, valid = [], [] - for i in idxs: - if i - SAC_WINDOW < 0: - valid.append(False) - continue - feats = np.concatenate( - [_nn_build_features(ind, k) for k in range(i - SAC_WINDOW, i)] + [np.array([0.0])] - ).astype(np.float32) - obs_list.append(feats) - valid.append(True) - deltas = [0.0] * len(idxs) - if obs_list: - try: - actions, _ = agent["model"].predict(np.stack(obs_list), deterministic=True) - actions = np.asarray(actions).reshape(len(obs_list), -1)[:, 0] - except Exception: - return None, None - j = 0 - for pos, ok in enumerate(valid): - if not ok: - continue - target_pos = float(np.clip(actions[j], -1.0, 1.0)) - j += 1 - d = 1 if target_pos > 0 else (-1 if target_pos < 0 else 0) - strength = min(abs(target_pos), 1.0) - deltas[pos] = d * strength * SAC_WEIGHT - return deltas, agent["n_samples"] - - -_CHRONOS_PIPE = None -_CHRONOS_LOCK = threading.Lock() -_CHRONOS_LAST_ERR = None - - -def _get_chronos_pipe(): - """Chronos modelini ilk kullanımda yükler, sonra bellekte tutar (iş parçacığı güvenli).""" - global _CHRONOS_PIPE - if _CHRONOS_PIPE is None: - with _CHRONOS_LOCK: - if _CHRONOS_PIPE is None: - _CHRONOS_PIPE = BaseChronosPipeline.from_pretrained( - CHRONOS_MODEL, device_map="cpu", torch_dtype=torch.float32) - return _CHRONOS_PIPE - - -_CHRONOS_FEAT_RANGE = { # tahmin sonrası fiziksel sınırlar (None = sınırsız) - "rsi": (0.0, 100.0), "stoch_k": (0.0, 100.0), "mfi": (0.0, 100.0), - "cmf": (-1.0, 1.0), "adx": (0.0, 100.0), "rvol": (0.0, None), "atr_pct": (0.0, None), -} - - -def _chronos_ctx(arr): - """Gösterge dizisini Chronos bağlamına çevirir: sonsuz/NaN -> ileri doldur, son CHRONOS_CONTEXT bar.""" - s = pd.Series(np.asarray(arr, dtype=float)).replace([np.inf, -np.inf], np.nan).ffill().dropna() - a = s.to_numpy()[-CHRONOS_CONTEXT:] - return a if len(a) >= CHRONOS_MIN_TRAIN else None - - -def _chronos_clip(name, v): - lo_, hi_ = _CHRONOS_FEAT_RANGE.get(name, (None, None)) - if lo_ is not None: - v = max(v, lo_) - if hi_ is not None: - v = min(v, hi_) - return v - - -def _chronos_filters(direction, now, pred): - """Tahmin edilen gösterge değerlerine göre fiyat tahmininin strength çarpanını ve itiraz listesini döndürür.""" - up, dn = direction > 0, direction < 0 - factor, notes = 1.0, [] - - def hit(f, label): - nonlocal factor - factor *= f - notes.append(label) - - if direction == 0: - return 1.0, notes - if "rsi" in pred and ((up and pred["rsi"] >= CHRONOS_RSI_OB) or (dn and pred["rsi"] <= CHRONOS_RSI_OS)): - hit(CHRONOS_RSI_DAMP, "RSI aşırı bölge") - if "stoch_k" in pred and ((up and pred["stoch_k"] >= CHRONOS_STOCH_OB) or (dn and pred["stoch_k"] <= CHRONOS_STOCH_OS)): - hit(CHRONOS_FILTER_DAMP, "Stokastik aşırı bölge") - if "mfi" in pred and ((up and pred["mfi"] >= CHRONOS_MFI_OB) or (dn and pred["mfi"] <= CHRONOS_MFI_OS)): - hit(CHRONOS_FILTER_DAMP, "MFI aşırı bölge") - if "macd_hist" in pred and ((up and pred["macd_hist"] < 0) or (dn and pred["macd_hist"] > 0)): - hit(CHRONOS_FILTER_DAMP, "MACD hist ters") - if "cmf" in pred and ((up and pred["cmf"] < -CHRONOS_CMF_MIN) or (dn and pred["cmf"] > CHRONOS_CMF_MIN)): - hit(CHRONOS_FILTER_DAMP, "CMF ters") - if "rvol" in pred and pred["rvol"] < CHRONOS_RVOL_MIN: - hit(CHRONOS_FILTER_DAMP, "hacim zayıf") - if "adx" in pred and pred["adx"] < CHRONOS_ADX_MIN: - hit(CHRONOS_FILTER_DAMP, "trend zayıf (ADX)") - if "atr_pct" in pred and now.get("atr_pct", 0.0) > 0 and pred["atr_pct"] / now["atr_pct"] >= CHRONOS_ATR_SPIKE: - hit(CHRONOS_FILTER_DAMP, "volatilite sıçraması (ATR)") - return max(factor, CHRONOS_MIN_FACTOR), notes - - -def get_chronos_forecast(df, symbol=None, bar=None, extras=None): - """Amazon Chronos-Bolt ile (sıfır-atış) fiyat tahmini, CHRONOS_FORWARD bar ileri. - Model YALNIZCA şu ana kadar görünen kapanışlarla beslenir (nedensel — gelecek sızıntısı yok). - Chronos kurulu değilse veya yeterli geçmiş yoksa None döner; sinyal motoru bu katman olmadan - da normal çalışmaya devam eder (diğer canlı katmanlarla aynı 'sessizce atla' mantığı). - extras = {"rsi": dizi, "stoch_k": dizi, "mfi": ..., "macd_hist": ..., "cmf": ..., "rvol": ..., - "adx": ..., "atr_pct": ...} verilirse hepsi fiyatla AYNI çağrıda (batch) tahmin edilir ve - _chronos_filters ile fiyat tahmininin strength'ini süzer.""" - if not _CHRONOS_AVAILABLE or len(df) < CHRONOS_MIN_TRAIN: - return None - - def _train(): - y = df["c"].dropna().astype(float).to_numpy()[-CHRONOS_CONTEXT:] - if len(y) < CHRONOS_MIN_TRAIN: - return None - ctx = [torch.tensor(y, dtype=torch.float32)] - names, feat_now = [], {} - for name, arr in (extras or {}).items(): - a = _chronos_ctx(arr) - if a is not None: - ctx.append(torch.tensor(a, dtype=torch.float32)) - names.append(name) - feat_now[name] = float(a[-1]) - with torch.no_grad(): - q, _ = _get_chronos_pipe().predict_quantiles( - ctx, prediction_length=CHRONOS_FORWARD, quantile_levels=[0.1, 0.5, 0.9]) - lo, yhat, hi = (float(v) for v in q[0, -1]) # q10 / medyan / q90 -> %80 band - cur_price = float(y[-1]) - chg = (yhat - cur_price) / cur_price if cur_price else 0.0 - band = max(hi - lo, 1e-9) - # tahmin edilen değişim, modelin kendi belirsizlik bandına kıyasla ne kadar büyük -> 0 (gürültü içinde) .. 1 (bandın çok dışında) - strength = min(abs(yhat - cur_price) / band, 1.0) - direction = 1 if yhat > cur_price else (-1 if yhat < cur_price else 0) - feat_pred = {n: _chronos_clip(n, float(q[i, -1, 1].item())) for i, n in enumerate(names, start=1)} - factor, notes = _chronos_filters(direction, feat_now, feat_pred) - strength *= factor - return { - "yhat": yhat, "yhat_lower": lo, "yhat_upper": hi, "change_pct": chg, - "direction": direction, "strength": strength, "n_samples": len(y), - "horizon_bars": CHRONOS_FORWARD, - "rsi_now": feat_now.get("rsi"), "rsi_pred": feat_pred.get("rsi"), - "feat_now": feat_now, "feat_pred": feat_pred, - "filters": notes, "filter_factor": factor, - } - - key = ("chronos_model", symbol, bar, len(df)) - global _CHRONOS_LAST_ERR - try: - _CHRONOS_LAST_ERR = None - return _cached(key, CHRONOS_CACHE_TTL, _train) - except Exception as e: - _CHRONOS_LAST_ERR = f"{type(e).__name__}: {e}" - print(f"[Chronos] tahmin başarısız: {_CHRONOS_LAST_ERR}", file=sys.stderr) - return None - - -# --------------------------------------------------------------------------- -# CANLI KATMAN — üst zaman dilimleri, emir defteri, funding (backtest EDİLMEZ) -# --------------------------------------------------------------------------- -def _mtf_score(symbol, bar, closed_only): - hdf = fetch_candles(symbol, bar, 300) - if closed_only: - hdf = hdf[hdf["confirm"] == 1].reset_index(drop=True) - hind = compute_indicators(hdf) - r = _score_at(hind, len(hdf) - 1) - return {"bar": bar, "score": r["score"]} - - -def _fetch_live_extras(symbol, bar, mtf, micro, closed_only): - """MTF / emir defteri / funding / v6 genişletilmiş veri çağrılarını PARALEL yapar; - hata veren parça sessizce atlanır.""" - tasks = {} - with ThreadPoolExecutor(max_workers=12) 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: - base_ccy = symbol.split("-")[0] - tasks["ob"] = ex.submit(fetch_orderbook, symbol, 20) - tasks["fund"] = ex.submit(fetch_funding_rate, symbol) - tasks["liq"] = ex.submit(fetch_liquidation_orders, symbol) - # --- v6: açık pozisyon, pozisyonlanma oranları, taker CVD, opsiyon, borsa-arası, makro --- - tasks["oi"] = ex.submit(fetch_open_interest, symbol) - tasks["ls_retail"] = ex.submit(fetch_long_short_ratio, base_ccy) - tasks["ls_top"] = ex.submit(fetch_long_short_ratio_top_trader, symbol) - tasks["taker"] = ex.submit(fetch_taker_volume, base_ccy) - tasks["opt_pcr"] = ex.submit(fetch_option_pcr, base_ccy) - tasks["cross_fund"] = ex.submit(fetch_cross_exchange_funding, base_ccy) - tasks["fng"] = ex.submit(fetch_fear_greed) - tasks["macro"] = ex.submit(fetch_macro_regime) - out = {} - for k, f in tasks.items(): - try: - out[k] = f.result() - except Exception: - out[k] = None - return out - - -def build_trade_plan(price, direction, atr_v, supports, resists, acct=None, risk_pct=None): - """Yapısal (destek/direnç) stop uygun aralıktaysa onu, değilse ATR stop'unu kullanır. - 3 kademeli hedef + hesap büyüklüğüne göre pozisyon boyutu.""" - d = direction - stop, stop_type = price - d * SL_ATR * atr_v, f"ATR × {SL_ATR}" - cand = None - if d > 0 and supports: - cand = supports[0][0] - 0.25 * atr_v - elif d < 0 and resists: - cand = resists[0][0] + 0.25 * atr_v - if cand is not None: - dist = abs(price - cand) - if 0.8 * atr_v <= dist <= 3.0 * atr_v and (cand < price if d > 0 else cand > price): - stop, stop_type = cand, "Yapısal (S/R arkası)" - risk = abs(price - stop) - plan = { - "direction": "LONG" if d > 0 else "SHORT", "entry": price, "stop_loss": stop, "stop_type": stop_type, - "risk_dist": risk, "risk_frac": risk / price, - "targets": [(price + d * risk * m, m) for m in TP_R_MULTIPLES], "sizing": None, - } - if acct and acct > 0 and risk_pct and risk_pct > 0 and risk > 0: - risk_amt = acct * risk_pct / 100.0 - qty = risk_amt / risk - notional = qty * price - plan["sizing"] = {"risk_amount": risk_amt, "qty": qty, "notional": notional, "leverage": notional / acct} - return plan - - -def compute_confidence(score, agree, mtf_state, hist_q, vol_q, nn_q=0.5, chronos_q=0.5, - lgbm_q=0.5, sac_q=0.5): - mag = min(abs(score) / 8.0, 1.0) - conf = 100.0 * (0.22 * mag + 0.15 * agree + 0.11 * mtf_state + 0.11 * hist_q - + 0.11 * nn_q + 0.10 * lgbm_q + 0.09 * chronos_q + 0.09 * sac_q + 0.02 * vol_q) - parts = {"Skor Büyüklüğü": mag, "Faktör Uyumu": agree, "MTF Teyidi": mtf_state, - "Tarihsel Kenar": hist_q, "Sinir Ağı Teyidi": nn_q, "LightGBM Teyidi": lgbm_q, - "Chronos Teyidi": chronos_q, "SAC Ajanı Teyidi": sac_q, "Volatilite Rejimi": vol_q} - return min(conf, 100.0), parts - - -def grade_from_conf(conf): - for thr, g, col in ((75, "A+", "#3ddc97"), (62, "A", "#7fe6b8"), (50, "B", "#f2b84b"), (38, "C", "#ff9aa8")): - if conf >= thr: - return g, col - return "D", "#ff5c72" - - -def generate_signal(df, symbol=None, bar=None, mtf=True, micro=True, closed_only=False, - acct=None, risk_pct=None): - ind = compute_indicators(df) - n = len(df) - start, scores = compute_score_series(ind, n) - base = _score_at(ind, n - 1, verbose=True) - score, price, w = base["score"], base["price"], base["w"] - reasons, contribs = list(base["reasons"]), list(base["contribs"]) - tags = [] - if base["squeeze_on"]: - tags.append(("SIKIŞMA AKTİF", "#f2b84b")) - for name, sgn in base["pat_hits"]: - tags.append((name.upper(), "#3ddc97" if sgn > 0 else "#ff5c72")) - - def overlay(label, delta, text): - nonlocal score - score += delta - reasons.append(("up" if delta > 0 else "down", text)) - contribs.append((label, delta)) - - # --- Uyumsuzluk (RSI divergence) — reversion sinyali, rejime göre ağırlıklı --- - div = detect_divergence(df, ind) - div_weight = 1.5 - 1.1 * w # güçlü trendde 0.4, yatayda 1.5 (eski davranışla aynı uçlar) - if div["bullish"]: - overlay("Yükseliş Uyumsuzluğu", div_weight, "Yükseliş uyumsuzluğu tespit edildi (fiyat düşük dip, RSI yüksek dip)") - tags.append(("YÜKSELİŞ UYUMSUZLUĞU", "#3ddc97")) - if div["bearish"]: - overlay("Düşüş Uyumsuzluğu", -div_weight, "Düşüş uyumsuzluğu tespit edildi (fiyat yüksek tepe, RSI düşük tepe)") - tags.append(("DÜŞÜŞ UYUMSUZLUĞU", "#ff5c72")) - - # --- Skor ivmesi --- - if n - 4 >= start: - momentum = base["score"] - scores[n - 4 - start]["score"] - if momentum > 1.5: - overlay("Skor İvmesi", 0.3, "Kompozit skor son barlarda hızla güçleniyor") - elif momentum < -1.5: - overlay("Skor İvmesi", -0.3, "Kompozit skor son barlarda hızla zayıflıyor") - - # --- Canlı ekstralar (paralel çekim) --- - extras = _fetch_live_extras(symbol, bar, mtf, micro, closed_only) - sgn = 1 if score > 0 else (-1 if score < 0 else 0) - - # --- 2 kademeli MTF teyidi --- - mtf_info, align_flags = [], [] - for key, wt in (("mtf1", 1.0), ("mtf2", 0.6)): - m = extras.get(key) - if not m: - continue - hs = m["score"] - aligned = None - if abs(hs) >= 1.0 and sgn != 0: - aligned = bool(np.sign(hs) == sgn) - align_flags.append(aligned) - if aligned: - overlay(f"{m['bar']} Teyidi", sgn * wt, f"{m['bar']} zaman diliminde de aynı yönlü teyit var (skor {hs:+.1f})") - else: - overlay(f"{m['bar']} Çelişkisi", -sgn * wt, f"{m['bar']} zaman dilimi ters yönlü sinyal veriyor (skor {hs:+.1f}) — dikkat") - mtf_info.append({"bar": m["bar"], "score": round(hs, 2), "aligned": aligned}) - mtf_state = (sum(1.0 for a in align_flags if a) / len(align_flags)) if align_flags else 0.5 - mtf_opposed = any(a is False for a in align_flags) - - # --- Emir defteri dengesizliği (derinlik ağırlıklı, kısa vadede anlamlı) --- - imb = None - ob = extras.get("ob") - if ob and ob.get("bids") and ob.get("asks"): - bw = sum(float(x[1]) / (k + 1) for k, x in enumerate(ob["bids"])) - aw = sum(float(x[1]) / (k + 1) for k, x in enumerate(ob["asks"])) - if bw + aw > 0: - imb = (bw - aw) / (bw + aw) - if abs(imb) >= 0.2: - delta = max(-0.8, min(0.8, imb * 2)) * (0.5 if bar in SLOW_BARS else 1.0) - overlay("Emir Defteri", delta, - f"Emir defterinde {'alış' if imb > 0 else 'satış'} baskısı (dengesizlik {imb * 100:+.0f}%)") - - # --- Funding rate (kalabalık pozisyon = kontraryen) --- - fr = extras.get("fund") - if fr is not None: - if fr > 0.0005: - overlay("Funding", -0.6, f"Funding aşırı pozitif ({fr * 100:.3f}%) — kalabalık long, tasfiye riski") - elif fr > 0.0002: - overlay("Funding", -0.3, f"Funding yüksek pozitif ({fr * 100:.3f}%) — long tarafı kalabalık") - elif fr < -0.0005: - overlay("Funding", 0.6, f"Funding aşırı negatif ({fr * 100:.3f}%) — kalabalık short, sıkışma potansiyeli") - elif fr < -0.0002: - overlay("Funding", 0.3, f"Funding negatif ({fr * 100:.3f}%) — short tarafı kalabalık") - - # --- Likidasyon kümeleri (v5): zorunlu tasfiye akışı --- - atr_for_liq = float(ind["atr"].iloc[-1]) if len(ind["atr"]) else 0.0 - liq_clusters = compute_liquidation_clusters(extras.get("liq") or [], price, atr_for_liq) - liq_imbalance = None - if liq_clusters: - total_long = sum(c["long_notional"] for c in liq_clusters) - total_short = sum(c["short_notional"] for c in liq_clusters) - if total_long + total_short > 0: - # +1 -> short tasfiyesi ağır bastı (kısa vadede yukarı sıkışma/momentum ipucu) - # -1 -> long tasfiyesi ağır bastı (kısa vadede aşağı kapitülasyon/momentum ipucu) - liq_imbalance = (total_short - total_long) / (total_long + total_short) - if abs(liq_imbalance) >= LIQ_IMBALANCE_MIN: - delta = max(-LIQ_IMBALANCE_WEIGHT, min(LIQ_IMBALANCE_WEIGHT, liq_imbalance * LIQ_IMBALANCE_WEIGHT)) - heavy_side, heavy_amt = ("short", total_short) if liq_imbalance > 0 else ("long", total_long) - overlay("Likidasyon Akışı", delta, - f"Son tasfiyelerde {heavy_side} pozisyonlar ağır bastı (~${fmt_large(heavy_amt)}) " - f"— kalan taraf sıkışmaya karşı daha kırılgan olabilir") - near = [c for c in liq_clusters if c["dist_atr"] <= LIQ_PROXIMITY_ATR] - if near: - top = max(near, key=lambda c: c["notional"]) - side_txt = "long tasfiye kümesi" if top["bias"] > 0 else "short tasfiye kümesi" - pos_txt = "üzerinde" if top["price"] > price else "altında" - reasons.append(("flat", f"Fiyata yakın ({top['dist_atr']:.1f} ATR {pos_txt}) yoğun bir {side_txt} var " - f"(~${fmt_large(top['notional'])}) — olası likidite mıknatısı/oynaklık bölgesi")) - - # --- v6: Açık Pozisyon (OI) + fiyat kombinasyonu — yeni pozisyon mu, kapanış mı? --- - oi_info = None - oi_snap = extras.get("oi") - if oi_snap: - oi_info = track_oi_history(symbol, oi_snap) - if oi_info and oi_info.get("change_pct") is not None and abs(oi_info["change_pct"]) * 100 >= OI_DELTA_MIN_PCT: - c_arr = ind["_arr"]["c"] - price_up = len(c_arr) >= 2 and c_arr[-1] > c_arr[-2] - oi_up = oi_info["change_pct"] > 0 - chg_txt = f"OI %{oi_info['change_pct'] * 100:+.1f}" - if oi_up and price_up: - overlay("Açık Pozisyon", OI_WEIGHT, f"{chg_txt} artarken fiyat da yükseliyor — taze long girişi, sağlam trend") - elif oi_up and not price_up: - overlay("Açık Pozisyon", -OI_WEIGHT, f"{chg_txt} artarken fiyat düşüyor — taze short girişi, sağlam düşüş") - elif not oi_up and price_up: - overlay("Açık Pozisyon", -OI_WEIGHT * 0.5, f"{chg_txt} azalırken fiyat yükseliyor — short kapanışı, kısa ömürlü olabilir") - else: - overlay("Açık Pozisyon", OI_WEIGHT * 0.5, f"{chg_txt} azalırken fiyat düşüyor — long kapanışı, kısa ömürlü olabilir") - - # --- v6: Long/Short pozisyon oranı — perakende vs. üst-yüzde-5 (akıllı para) ayrışması --- - ls_retail = extras.get("ls_retail") - ls_top = extras.get("ls_top") - if ls_retail is not None: - if ls_retail >= LS_RETAIL_EXTREME: - overlay("Perakende Pozisyonlanma", -0.4, f"Perakende long/short oranı aşırı yüksek ({ls_retail:.2f}) — kalabalık long, kontraryen risk") - elif ls_retail <= 1 / LS_RETAIL_EXTREME: - overlay("Perakende Pozisyonlanma", 0.4, f"Perakende long/short oranı aşırı düşük ({ls_retail:.2f}) — kalabalık short, kontraryen risk") - if ls_retail is not None and ls_top is not None: - retail_dir = 1 if ls_retail > 1.0 else -1 - top_dir = 1 if ls_top > 1.0 else -1 - if retail_dir != top_dir: - overlay("Akıllı Para Ayrışması", LS_DIVERGENCE_WEIGHT * top_dir, - f"Perakende ({ls_retail:.2f}) ve üst-yüzde-5 trader ({ls_top:.2f}) pozisyonları ters yönde — " - f"üst trader {'long' if top_dir > 0 else 'short'} ağırlıklı, perakendeye karşı gidiyor") - tags.append(("AKILLI PARA AYRIŞMASI", "#f2b84b")) - - # --- v6: Taker CVD — agresif alış/satış hacmi dengesizliği --- - taker = extras.get("taker") - taker_cvd = None - if taker and (taker["buy"] + taker["sell"]) > 0: - taker_cvd = (taker["buy"] - taker["sell"]) / (taker["buy"] + taker["sell"]) - if abs(taker_cvd) >= TAKER_CVD_MIN_RATIO: - delta = max(-TAKER_CVD_WEIGHT, min(TAKER_CVD_WEIGHT, taker_cvd * 2)) - overlay("Taker CVD", delta, - f"Agresif taker hacminde {'alış' if taker_cvd > 0 else 'satış'} baskın " - f"(dengesizlik {taker_cvd * 100:+.0f}%) — gerçek yön baskısı") - - # --- v6: Opsiyon Put/Call açık pozisyon oranı (yalnızca BTC/ETH gibi opsiyon piyasası olanlarda) --- - opt_pcr_data = extras.get("opt_pcr") - if opt_pcr_data: - pcr = opt_pcr_data["pcr"] - if pcr >= OPT_PCR_EXTREME_HIGH: - overlay("Opsiyon PCR", OPT_PCR_WEIGHT, f"Put/Call açık pozisyon oranı yüksek ({pcr:.2f}) — opsiyon piyasasında aşırı korku, kontraryen boğa ipucu") - elif pcr <= OPT_PCR_EXTREME_LOW: - overlay("Opsiyon PCR", -OPT_PCR_WEIGHT, f"Put/Call açık pozisyon oranı düşük ({pcr:.2f}) — opsiyon piyasasında aşırı iyimserlik, kontraryen ayı ipucu") - - # --- v6: Borsalar arası funding uzlaşısı (Binance/Bybit ile karşılaştırma) --- - cross_fund = extras.get("cross_fund") - cross_fund_avg = None - if cross_fund: - vals = list(cross_fund.values()) - if vals: - cross_fund_avg = float(np.mean(vals)) - if fr is not None and abs(cross_fund_avg) > 0.0002: - same_sign = (cross_fund_avg > 0) == (fr > 0) - if same_sign: - overlay("Borsalar Arası Funding", (-1 if fr > 0 else 1) * CROSS_FUNDING_WEIGHT, - f"Binance/Bybit funding'i de OKX ile aynı yönde ({cross_fund_avg * 100:+.4f}% ort.) — " - f"pozisyonlanma tek borsaya özgü değil, borsa geneli") - else: - reasons.append(("flat", f"OKX funding ({fr * 100:+.4f}%) diğer borsalardan ({cross_fund_avg * 100:+.4f}% ort.) ayrışıyor — tek borsaya özgü pozisyonlanma olabilir")) - - # --- v6: Fear & Greed Index — piyasa geneli duygu, aşırı uçlarda kontraryen --- - fng = extras.get("fng") - if fng: - if fng["value"] <= FNG_EXTREME_FEAR: - overlay("Fear & Greed", FNG_WEIGHT, f"Piyasa geneli 'Aşırı Korku' bölgesinde ({fng['value']}/100) — tarihsel olarak kontraryen dip bölgesi") - elif fng["value"] >= FNG_EXTREME_GREED: - overlay("Fear & Greed", -FNG_WEIGHT, f"Piyasa geneli 'Aşırı Açgözlülük' bölgesinde ({fng['value']}/100) — tarihsel olarak kontraryen tepe bölgesi") - - # --- v6: Makro rejim (DXY / ABD 10Y / SPX) — kripto risk-on/risk-off bağlamı --- - macro = extras.get("macro") - if macro and macro.get("bias"): - bias = macro["bias"] - mag = min(abs(bias), 2) / 2.0 - if bias > 0: - overlay("Makro Rejim", MACRO_WEIGHT * mag, "Makro arka plan risk-on (DXY zayıf ve/veya SPX güçlü) — risk varlıklarını destekliyor") - else: - overlay("Makro Rejim", -MACRO_WEIGHT * mag, "Makro arka plan risk-off (DXY güçlü ve/veya SPX zayıf) — risk varlıkları için rüzgar ters") - - # --- Sinir Ağı (v4): öğrenilmiş yön olasılığı — sadece start'tan önceki barlarla eğitilir --- - nn_result, nn_prob, nn_dir, nn_strength = None, None, 0, 0.0 - try: - nn_result = get_nn_model(ind, n, symbol, bar) - except Exception: - nn_result = None - if nn_result: - nn_prob = float(nn_result["model"].predict_proba(_nn_build_features(ind, n - 1))[0]) - nn_dir = 1 if nn_prob > 0.5 else -1 - nn_strength = abs(nn_prob - 0.5) * 2.0 # 0 (kararsız) .. 1 (çok emin) - sgn = 1 if score > 0 else (-1 if score < 0 else 0) - nn_delta = nn_dir * nn_strength * NN_WEIGHT - if sgn != 0 and nn_dir == sgn: - overlay("Sinir Ağı", nn_delta, - f"NN modeli aynı yönü destekliyor (P(yukarı)={nn_prob * 100:.0f}%, " - f"eğitim seti kaba isabeti %{nn_result['train_acc'] * 100:.0f}, n={nn_result['n_samples']})") - elif sgn != 0: - overlay("Sinir Ağı", nn_delta, - f"NN modeli ters yönde (P(yukarı)={nn_prob * 100:.0f}%, " - f"eğitim seti kaba isabeti %{nn_result['train_acc'] * 100:.0f}, n={nn_result['n_samples']}) — dikkat") - else: - reasons.append(("flat", f"NN modeli için yeterli geçmiş veri yok (en az {NN_MIN_TRAIN} bar gerekli) — bu katman devre dışı")) - - # --- LightGBM (v8): ağaç-tabanlı öğrenilmiş yön olasılığı — NN ile aynı veri, farklı algoritma --- - lgbm_result, lgbm_prob, lgbm_dir, lgbm_strength = None, None, 0, 0.0 - try: - lgbm_result = get_lgbm_model(ind, n, symbol, bar) - except Exception: - lgbm_result = None - if lgbm_result: - lgbm_prob = float(lgbm_result["model"].predict_proba( - _nn_build_features(ind, n - 1).reshape(1, -1))[0, 1]) - lgbm_dir = 1 if lgbm_prob > 0.5 else -1 - lgbm_strength = abs(lgbm_prob - 0.5) * 2.0 - sgn = 1 if score > 0 else (-1 if score < 0 else 0) - lgbm_delta = lgbm_dir * lgbm_strength * LGBM_WEIGHT - if sgn != 0 and lgbm_dir == sgn: - overlay("LightGBM", lgbm_delta, - f"LightGBM modeli aynı yönü destekliyor (P(yukarı)={lgbm_prob * 100:.0f}%, " - f"eğitim seti kaba isabeti %{lgbm_result['train_acc'] * 100:.0f}, n={lgbm_result['n_samples']})") - elif sgn != 0: - overlay("LightGBM", lgbm_delta, - f"LightGBM modeli ters yönde (P(yukarı)={lgbm_prob * 100:.0f}%, " - f"eğitim seti kaba isabeti %{lgbm_result['train_acc'] * 100:.0f}, n={lgbm_result['n_samples']}) — dikkat") - else: - note = ("LightGBM kurulu değil" if not _LGBM_AVAILABLE - else f"LightGBM için yeterli geçmiş veri yok (en az {LGBM_MIN_TRAIN} bar gerekli)") - reasons.append(("flat", f"{note} — bu katman devre dışı")) - - # --- Chronos (v7): zaman serisi temel modeli tahmini — sadece geçmiş barlarla beslenir --- - chronos_result = None - try: - chronos_result = get_chronos_forecast(df, symbol, bar, { - "rsi": ind["rsi"].to_numpy(), - "stoch_k": ind["stoch_k"].to_numpy(), - "mfi": ind["mfi"].to_numpy(), - "macd_hist": ind["macd_hist"].to_numpy(), - "cmf": ind["cmf"].to_numpy(), - "rvol": ind["rvol"].to_numpy(), - "adx": ind["adx"].to_numpy(), - "atr_pct": (ind["atr"] / df["c"]).to_numpy(), - }) - except Exception: - chronos_result = None - if chronos_result: - p_dir, p_str = chronos_result["direction"], chronos_result["strength"] - p_delta = p_dir * p_str * CHRONOS_WEIGHT - rsi_txt = (f", RSI {chronos_result['rsi_now']:.0f}→{chronos_result['rsi_pred']:.0f}" - if chronos_result.get("rsi_pred") is not None else "") - if chronos_result.get("filters"): - rsi_txt += (f", süzgeç: {', '.join(chronos_result['filters'])} " - f"(güç ×{chronos_result['filter_factor']:.2f})") - sgn = 1 if score > 0 else (-1 if score < 0 else 0) - if p_dir != 0 and sgn != 0 and p_dir == sgn: - overlay("Chronos Tahmini", p_delta, - f"Chronos {chronos_result['horizon_bars']} bar ileri için aynı yönü işaret ediyor " - f"(beklenen değişim {chronos_result['change_pct'] * 100:+.2f}%, n={chronos_result['n_samples']}{rsi_txt})") - elif p_dir != 0 and sgn != 0: - overlay("Chronos Tahmini", p_delta, - f"Chronos {chronos_result['horizon_bars']} bar ileri için ters yönü işaret ediyor " - f"(beklenen değişim {chronos_result['change_pct'] * 100:+.2f}%, n={chronos_result['n_samples']}{rsi_txt}) — dikkat") - else: - note = ("Chronos (torch/chronos-forecasting) kurulu değil" if not _CHRONOS_AVAILABLE - else f"Chronos hata verdi ({_CHRONOS_LAST_ERR})" if _CHRONOS_LAST_ERR - else f"Chronos için yeterli geçmiş veri yok (en az {CHRONOS_MIN_TRAIN} bar gerekli)") - reasons.append(("flat", f"{note} — bu katman devre dışı")) - - # --- FinRL + SAC (v9): otomatik eğitilen RL ajanının önerdiği hedef pozisyon --- - sac_result = None - try: - sac_agent = get_sac_agent(ind, n, symbol, bar) - if sac_agent and n - SAC_WINDOW >= 0: - obs = np.concatenate( - [_nn_build_features(ind, k) for k in range(n - SAC_WINDOW, n)] + [np.array([0.0])] - ).astype(np.float32) - action, _ = sac_agent["model"].predict(obs, deterministic=True) - target_pos = float(np.clip(np.asarray(action).reshape(-1)[0], -1.0, 1.0)) - sac_dir = 1 if target_pos > 0 else (-1 if target_pos < 0 else 0) - sac_strength = min(abs(target_pos), 1.0) - sac_result = {"target_pos": target_pos, "direction": sac_dir, "strength": sac_strength, - "n_samples": sac_agent["n_samples"]} - except Exception: - sac_result = None - if sac_result and sac_result["direction"] != 0: - sgn = 1 if score > 0 else (-1 if score < 0 else 0) - sac_delta = sac_result["direction"] * sac_result["strength"] * SAC_WEIGHT - if sgn != 0 and sac_result["direction"] == sgn: - overlay("FinRL SAC Ajanı", sac_delta, - f"SAC ajanı aynı yönde pozisyon öneriyor (hedef pozisyon {sac_result['target_pos']:+.2f}, " - f"n={sac_result['n_samples']} bar üzerinde otomatik eğitildi)") - elif sgn != 0: - overlay("FinRL SAC Ajanı", sac_delta, - f"SAC ajanı ters yönde pozisyon öneriyor (hedef pozisyon {sac_result['target_pos']:+.2f}, " - f"n={sac_result['n_samples']} bar üzerinde otomatik eğitildi) — dikkat") - else: - note = ("FinRL/SAC (gymnasium/stable-baselines3) kurulu değil" if not _SAC_AVAILABLE - else f"SAC ajanı için yeterli geçmiş veri yok (en az {SAC_MIN_TRAIN + SAC_WINDOW} bar gerekli)") - reasons.append(("flat", f"{note} — bu katman devre dışı")) - - # --- Destek / direnç yakınlığı --- - atr_v = float(ind["atr"].iloc[-1]) - supports, resists = find_sr_levels(df, atr_v) - sgn = 1 if score > 0 else (-1 if score < 0 else 0) - if atr_v > 0 and sgn != 0: - if sgn > 0: - if resists and resists[0][1] >= 2 and (resists[0][0] - price) / atr_v <= 1.0: - lv, tc = resists[0] - overlay("Yakın Direnç", -0.8, f"Yakın direnç {fmt_price(lv)} ({tc} temas, {(lv - price) / atr_v:.1f} ATR) — yükselişi sınırlayabilir") - elif supports and supports[0][1] >= 2 and (price - supports[0][0]) / atr_v <= 0.5: - lv, tc = supports[0] - overlay("Destek Üstünde", 0.4, f"Fiyat güçlü destek {fmt_price(lv)} ({tc} temas) üzerinde") - else: - if supports and supports[0][1] >= 2 and (price - supports[0][0]) / atr_v <= 1.0: - lv, tc = supports[0] - overlay("Yakın Destek", 0.8, f"Yakın destek {fmt_price(lv)} ({tc} temas, {(price - lv) / atr_v:.1f} ATR) — düşüşü sınırlayabilir") - elif resists and resists[0][1] >= 2 and (resists[0][0] - price) / atr_v <= 0.5: - lv, tc = resists[0] - overlay("Direnç Altında", -0.4, f"Fiyat güçlü direnç {fmt_price(lv)} ({tc} temas) altında") - - score = max(-10.0, min(10.0, score)) - - # --- Konsensüs + sınıflandırma --- - direction = 1 if score > 0 else -1 - tot = base["bull_w"] + base["bear_w"] - agree = ((base["bull_w"] if direction > 0 else base["bear_w"]) / tot) if tot > 0 else 0.5 - if abs(score) >= STRONG_THRESHOLD and agree >= 0.65 and not mtf_opposed: - signal, color = ("GÜÇLÜ AL", "#3ddc97") if score > 0 else ("GÜÇLÜ SAT", "#ff5c72") - elif abs(score) >= SIGNAL_THRESHOLD: - signal, color = ("AL", "#7fe6b8") if score > 0 else ("SAT", "#ff9aa8") - if abs(score) >= STRONG_THRESHOLD: - reasons.append(("flat", f"Skor güçlü bölgede ancak konsensüs ({agree * 100:.0f}%) veya MTF filtresini geçemedi — GÜÇLÜ yerine normal sinyal verildi")) - else: - signal, color = "BEKLE / NÖTR", "#f2b84b" - - # --- v3 SERT FİLTRELER — düşük kaliteli AL/SAT'ları BEKLE'ye çevirir (isabet oranını artırmak için) --- - veto_reasons = [] - adx_v = base["adx"] - if signal != "BEKLE / NÖTR": - if adx_v == adx_v and adx_v < MIN_REGIME_ADX and not base["squeeze_on"]: - veto_reasons.append(f"ADX çok düşük ({adx_v:.1f}) — belirgin bir trend/rejim yok, yön güvenilir değil") - if agree < MIN_AGREE_FOR_SIGNAL: - veto_reasons.append(f"Faktör konsensüsü zayıf (%{agree * 100:.0f}) — göstergeler birbiriyle çelişiyor") - if MTF_HARD_GATE and align_flags and align_flags[0] is False and "GÜÇLÜ" not in signal: - veto_reasons.append(f"Üst zaman dilimi ({mtf_info[0]['bar']}) net ters yönde — teyit yok") - if imb is not None and np.sign(imb) not in (0, direction) and abs(imb) >= OB_HARD_GATE_IMB: - veto_reasons.append(f"Emir defterinde sinyale güçlü ters yönde baskı var (dengesizlik {imb * 100:+.0f}%)") - if nn_result and nn_dir != direction and nn_strength >= NN_GATE_STRENGTH: - veto_reasons.append(f"NN modeli güçlü şekilde ters yönde (P(yukarı)={nn_prob * 100:.0f}%)") - if lgbm_result and lgbm_dir != direction and lgbm_strength >= LGBM_GATE_STRENGTH: - veto_reasons.append(f"LightGBM modeli güçlü şekilde ters yönde (P(yukarı)={lgbm_prob * 100:.0f}%)") - if chronos_result and chronos_result["direction"] != direction and chronos_result["strength"] >= CHRONOS_GATE_STRENGTH: - veto_reasons.append(f"Chronos güçlü şekilde ters yönde (beklenen değişim {chronos_result['change_pct'] * 100:+.2f}%)") - if sac_result and sac_result["direction"] != 0 and sac_result["direction"] != direction and sac_result["strength"] >= SAC_GATE_STRENGTH: - veto_reasons.append(f"FinRL SAC ajanı güçlü şekilde ters yönde (hedef pozisyon {sac_result['target_pos']:+.2f})") - if veto_reasons: - for vr in veto_reasons: - reasons.append(("flat", f"FİLTRE: {vr}")) - signal, color = "BEKLE / NÖTR", "#f2b84b" - tags.append(("SERT FİLTRE İLE ELENDİ", "#7c8797")) - - is_neutral = signal == "BEKLE / NÖTR" - - # --- Tarihsel doğrulama --- - backtest = run_backtest(df, ind, start, scores, symbol=symbol, bar=bar) - 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)) - if acc.get("stable") is False: - qs.append(0.2) # ilk/ikinci yarı isabeti tutarsız -> rejim kayması / overfit riski - reasons.append(("flat", f"Tarihsel isabet tutarsız: ilk yarı %{acc['half1_acc']*100:.0f} vs " - f"ikinci yarı %{acc['half2_acc']*100:.0f} — bu kenara temkinli yaklaşın")) - st = backtest["stats"] - if st: - qs.append(min(max((st["profit_factor"] - 0.8) / 0.8, 0.0), 1.0)) - if qs: - hist_q = float(np.mean(qs)) - rank = base["atr_rank"] - vol_q = 1.0 if 0.10 <= rank <= 0.90 else 0.5 - if rank > 0.90: - reasons.append(("flat", f"Volatilite aşırı yüksek (ATR yüzdeliği %{rank * 100:.0f}) — stoplar geniş tutulmalı, güven düşürüldü")) - nn_q = 0.5 - if nn_result: - nn_agree = 1.0 if nn_dir == direction else -1.0 - nn_q = min(max(0.5 + nn_agree * nn_strength * (0.5 + 0.5 * nn_result["train_acc"]), 0.0), 1.0) - lgbm_q = 0.5 - if lgbm_result: - lgbm_agree = 1.0 if lgbm_dir == direction else -1.0 - lgbm_q = min(max(0.5 + lgbm_agree * lgbm_strength * (0.5 + 0.5 * lgbm_result["train_acc"]), 0.0), 1.0) - chronos_q = 0.5 - if chronos_result: - p_agree = 1.0 if chronos_result["direction"] == direction else -1.0 - chronos_q = min(max(0.5 + p_agree * chronos_result["strength"] * 0.5, 0.0), 1.0) - sac_q = 0.5 - if sac_result and sac_result["direction"] != 0: - sac_agree = 1.0 if sac_result["direction"] == direction else -1.0 - sac_q = min(max(0.5 + sac_agree * sac_result["strength"] * 0.5, 0.0), 1.0) - conf, conf_parts = compute_confidence(score, agree, mtf_state, hist_q, vol_q, nn_q, chronos_q, lgbm_q, sac_q) - grade, grade_color = ("—", "#7c8797") if is_neutral else grade_from_conf(conf) - - plan = None - if not is_neutral and atr_v > 0: - plan = build_trade_plan(price, direction, atr_v, supports, resists, acct, risk_pct) - - return { - "signal": signal, "score": round(score, 2), "confidence": round(conf, 1), "grade": grade, - "grade_color": grade_color, "color": color, "reasons": reasons, "contribs": contribs, - "indicators": ind, "price": price, "divergence": div, "mtf": mtf_info, "plan": plan, - "backtest": backtest, "regime": base["regime"], "adx": base["adx"], "atr_rank": rank, - "squeeze_on": base["squeeze_on"], "agree": agree, "bull_w": base["bull_w"], "bear_w": base["bear_w"], - "conf_parts": conf_parts, "tags": tags, "funding": fr, "imbalance": imb, - "supports": supports, "resists": resists, "atr": atr_v, - "liq_clusters": liq_clusters, "liq_imbalance": liq_imbalance, - "nn": ({"prob_up": round(nn_prob, 3), "train_acc": round(nn_result["train_acc"], 3), - "n_samples": nn_result["n_samples"]} if nn_result else None), - "lgbm": ({"prob_up": round(lgbm_prob, 3), "train_acc": round(lgbm_result["train_acc"], 3), - "n_samples": lgbm_result["n_samples"]} if lgbm_result else None), - "chronos": ({"yhat": chronos_result["yhat"], "change_pct": round(chronos_result["change_pct"], 4), - "horizon_bars": chronos_result["horizon_bars"], "n_samples": chronos_result["n_samples"], - "rsi_now": chronos_result["rsi_now"], "rsi_pred": chronos_result["rsi_pred"], - "feat_now": chronos_result["feat_now"], "feat_pred": chronos_result["feat_pred"], - "filters": chronos_result["filters"], "filter_factor": chronos_result["filter_factor"]} - if chronos_result else None), - "sac": ({"target_pos": round(sac_result["target_pos"], 3), "n_samples": sac_result["n_samples"]} - if sac_result else None), - # --- v6 genişletilmiş veri katmanı (UI kartları için) --- - "symbol_base": symbol.split("-")[0] if symbol else None, - "oi": oi_info["oi"] if oi_info else None, - "oi_chg": oi_info["change_pct"] if oi_info else None, - "oi_n": oi_info["n_samples"] if oi_info else None, - "ls_retail": ls_retail, "ls_top": ls_top, "taker_cvd": taker_cvd, - "opt_pcr": opt_pcr_data["pcr"] if opt_pcr_data else None, - "cross_fund_avg": cross_fund_avg, "fng": fng, "macro": macro, - } - - -# --------------------------------------------------------------------------- -# 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'