CRT Analyzer Pro: Binance USDT Backtest & 25-Coin Filter

🧩 Syntax:
# -*- coding: utf-8 -*-
"""
CRT Analyzer v3 PRO - Tek Dosya (Aktif=25, Süresiz Blacklist, Açılışta Otomatik Eleme)
Gerekenler: Python 3.10+, pip install pandas numpy ccxt psutil requests
Timeframe'ler: 15m, 1h, 4h
Bar sayısı: DEFAULT_BARS (varsayılan 1000; sistem kaldırırsa 5000 yapabilirsin)

Yapılanlar:
- Binance USDT spot evrenini indir (hacme göre sırala).
- Süresiz blacklist'i oku; aday havuzdan çıkar.
- 25 coin hedefi için sırayla hafif backtest yap (batch ve beklemeli).
- Winrate ve AvgPnL eşiğini geçenler active_list'e eklenir, geçmeyenler blacklist'e yazılır.
- 25 tamamlanana kadar dış aday çekmeye devam eder.
- Telegram'a özet mesajlar gönderir (başladı / eklenenler / blackliste düşenler / dinlenmeye geçti vs.)

Dikkat:
- TOKEN ve CHAT_ID değerlerini aşağıda doldur.
"""

import os, json, time, math, asyncio, statistics
from datetime import datetime, timezone, timedelta

import numpy as np
import pandas as pd
import ccxt.async_support as ccxt
import requests
import psutil

# ===================== K U R U L U M  /  A Y A R L A R =====================

# >>>>> BURAYA KENDİ TOKEN VE CHAT_ID'NI YAZ <<<<<
MAIN_BOT_TOKEN   = os.environ.get("TG_MAIN_BOT_TOKEN",   "PASTE_YOURS")  # örn: 8207581334:AAF5IO-...
RESULT_BOT_TOKEN = os.environ.get("TG_RESULT_BOT_TOKEN", "PASTE_YOURS")  # ikinci bot istersen
MAIN_CHAT_ID     = int(os.environ.get("TG_MAIN_CHAT_ID", "1541732373"))   # senin chat id

# Hedef aktif sayısı
TARGET_ACTIVE = 25

# Timeframe'ler ve bar
TIMEFRAMES = ["15m", "1h", "4h"]
DEFAULT_BARS = 1000  # sistemin güçlüyse 5000 yap

# Eşikler
WINRATE_TH     = 60.0   # %
AVG_PNL_TH     = 0.5    # % (işlem başına)
MIN_TRADES_TH  = 10

# Kaynak koruma
CPU_MAX = 85          # % üstü ise dinlen
RAM_MIN_FREE_GB = 2.0 # 2 GB'den az boş RAM kalırsa dinlen
REST_SECONDS = 30     # dinlenme süresi

# Batch ayarları
BATCH_SIZE = 6            # aynı anda kaç coin test edilecek
API_DELAY_RANGE = (0.15, 0.6)  # veri çekiş araları

# Klasörler
BASE_DIR = os.path.join(os.getcwd(), "crt_data")
CACHE_DIR = os.path.join(BASE_DIR, "cache")
BK_DIR = os.path.join(BASE_DIR, "backtest")
os.makedirs(BASE_DIR, exist_ok=True)
os.makedirs(CACHE_DIR, exist_ok=True)
os.makedirs(BK_DIR, exist_ok=True)

# Dosyalar
ACTIVE_FILE    = os.path.join(BASE_DIR, "active_list.json")
BLACKLIST_FILE = os.path.join(BASE_DIR, "blacklist.json")
UNIVERSE_FILE  = os.path.join(BASE_DIR, "coin_universe.json")
SCAN_FILE      = os.path.join(BASE_DIR, "scan_results.json")
ERROR_LOG      = os.path.join(BASE_DIR, "error.log")


# =====================  Y A R D I M C I   F O N K S I Y O N L A R =====================

def nowu():
    return datetime.now(timezone.utc)

def tzs(dt):
    return dt.astimezone().strftime("%Y-%m-%d %H:%M:%S %Z")

def logerr(e: Exception):
    try:
        with open(ERROR_LOG, "a", encoding="utf-8") as f:
            f.write(f"{nowu().isoformat()} :: {repr(e)}\n")
    except:
        pass

def send_tg(token, chat_id, text):
    try:
        url = f"https://api.telegram.org/bot{token}/sendMessage"
        requests.post(url, json={"chat_id": chat_id, "text": text, "parse_mode": "Markdown"})
    except Exception as e:
        logerr(e)

def loadj(path, default):
    try:
        if os.path.exists(path):
            with open(path, "r", encoding="utf-8") as f:
                return json.load(f)
    except Exception as e:
        logerr(e)
    return default

def savej(path, data):
    try:
        with open(path, "w", encoding="utf-8") as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
    except Exception as e:
        logerr(e)

def wait_system_sync():
    """CPU/RAM yüksekse bloklayarak dinlenir."""
    while True:
        try:
            cpu = psutil.cpu_percent(interval=1.0)
            avail_gb = psutil.virtual_memory().available / (1024**3)
            if cpu < CPU_MAX and avail_gb > RAM_MIN_FREE_GB:
                return
            # dinlenme
            send_tg(MAIN_BOT_TOKEN, MAIN_CHAT_ID,
                    f"⏸️ Dinlenme: CPU={cpu:.0f}% | Boş RAM={avail_gb:.1f} GB. {REST_SECONDS}s bekleniyor…")
            time.sleep(REST_SECONDS)
        except Exception as e:
            logerr(e)
            time.sleep(REST_SECONDS)
            return

def rand_delay():
    a, b = API_DELAY_RANGE
    time.sleep(np.random.uniform(a, b))


# =====================  G Ö S T E R G E L E R  (Pandas/Numpy) =====================

def rsi(series: pd.Series, period=14):
    # Wilder RSI
    delta = series.diff()
    up = np.where(delta > 0, delta, 0.0)
    down = np.where(delta < 0, -delta, 0.0)
    roll_up = pd.Series(up, index=series.index).ewm(alpha=1/period, adjust=False).mean()
    roll_down = pd.Series(down, index=series.index).ewm(alpha=1/period, adjust=False).mean()
    rs = roll_up / (roll_down.replace(0, np.nan))
    rsi = 100 - (100 / (1 + rs))
    return rsi.fillna(50.0)

def atr(high: pd.Series, low: pd.Series, close: pd.Series, period=14):
    prev_close = close.shift(1)
    tr = pd.concat([
        (high - low).abs(),
        (high - prev_close).abs(),
        (low - prev_close).abs()
    ], axis=1).max(axis=1)
    atr = tr.ewm(alpha=1/period, adjust=False).mean()
    return atr

def ema(series: pd.Series, period=200):
    return series.ewm(span=period, adjust=False).mean()

def adx(high, low, close, period=14):
    # +DM, -DM
    up_move = high.diff()
    down_move = low.diff().mul(-1)

    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([
        (high - low).abs(),
        (high - close.shift(1)).abs(),
        (low - close.shift(1)).abs()
    ], axis=1).max(axis=1)

    atr14 = tr.ewm(alpha=1/period, adjust=False).mean()

    plus_di = 100 * (pd.Series(plus_dm, index=high.index).ewm(alpha=1/period, adjust=False).mean() / atr14.replace(0,np.nan))
    minus_di = 100 * (pd.Series(minus_dm, index=high.index).ewm(alpha=1/period, adjust=False).mean() / atr14.replace(0,np.nan))

    dx = ( (plus_di - minus_di).abs() / (plus_di + minus_di).replace(0, np.nan) ) * 100
    adx_val = dx.ewm(alpha=1/period, adjust=False).mean()
    return adx_val.fillna(20.0)


# =====================  V E R I   C E K M E  &  O H L C V  =====================

def tf_to_ms(tf: str) -> int:
    if tf.endswith("m"):
        return int(tf[:-1]) * 60 * 1000
    if tf.endswith("h"):
        return int(tf[:-1]) * 60 * 60 * 1000
    if tf.endswith("d"):
        return int(tf[:-1]) * 24 * 60 * 60 * 1000
    raise ValueError("unsupported tf")

async def fetch_ohlcv_full(ex, symbol: str, timeframe: str, bars: int) -> pd.DataFrame:
    """
    Binance limitleri nedeniyle art arda çekiyoruz.
    ccxt limit genelde 1500; burada 1000'lik parçalarla gidiyoruz.
    """
    limit_per_call = 1000
    need = bars
    since = None
    out = []

    while need > 0:
        try:
            if since is None:
                # en sonlardan başlayıp geriye doğru değil, ileri doğru almak için since'i geçmişe çekebiliriz;
                # ancak ccxt fetch_ohlcv genelde since'ten ileri alır. Burada since None ise son barları alır.
                o = await ex.fetch_ohlcv(symbol, timeframe=timeframe, limit=min(limit_per_call, need))
            else:
                o = await ex.fetch_ohlcv(symbol, timeframe=timeframe, since=since, limit=min(limit_per_call, need))
            if not o:
                break
            out.extend(o)
            # ileri kaydır
            since = o[-1][0] + 1
            need -= len(o)
            rand_delay()
        except Exception as e:
            logerr(e)
            rand_delay()
            break

    if not out:
        return pd.DataFrame(columns=["time","open","high","low","close","volume"])

    df = pd.DataFrame(out, columns=["time","open","high","low","close","volume"])
    df["time"] = pd.to_datetime(df["time"], unit="ms", utc=True)
    for c in ["open","high","low","close","volume"]:
        df[c] = pd.to_numeric(df[c], errors="coerce")
    df = df.dropna().drop_duplicates(subset=["time"]).set_index("time").sort_index()
    return df


# =====================  B A S I T   S T R A T E J I   &   B A C K T E S T =====================

def simple_signals_and_pnl(df: pd.DataFrame, tf: str):
    """
    Basit ve sağlam bir kriter:
    - Trend: close vs EMA200 (aynı TF)
    - Momentum: ADX(14) > 20
    - Giriş bandı: BUY -> RSI 50..68, SELL -> RSI 32..50
    - TP/SL: ATR(14) x 1.0 / 1.0
    - LOOKAHEAD: 3 bar boyunca TP/SL'ye hangisi önce değerse o sayılır.

    Dönen:
      trades, wins, avg_pnl_pct
    """
    if len(df) < 250:
        return 0, 0, 0.0

    df = df.copy()
    df["rsi"] = rsi(df["close"], 14)
    df["atr"] = atr(df["high"], df["low"], df["close"], 14)
    df["ema200"] = ema(df["close"], 200)
    df["adx"] = adx(df["high"], df["low"], df["close"], 14)

    trades = 0
    wins = 0
    pnls = []

    LOOK = 3
    ATR_MULT = 1.0

    closes = df["close"].values
    highs  = df["high"].values
    lows   = df["low"].values
    rsis   = df["rsi"].values
    atrs   = df["atr"].values
    ema200s= df["ema200"].values
    adxs   = df["adx"].values
    idxs   = np.arange(len(df))

    for i in range(220, len(df) - LOOK - 1):
        c = closes[i]; a = atrs[i]; r = rsis[i]; e = ema200s[i]; ad = adxs[i]
        if np.isnan([c,a,r,e,ad]).any():
            continue
        # BUY sinyali
        if c > e and ad > 20 and 50 <= r <= 68:
            entry = c; tp = entry + ATR_MULT * a; sl = entry - ATR_MULT * a
            hit = None
            for j in range(1, LOOK+1):
                hi = highs[i+j]; lo = lows[i+j]
                if lo <= sl: hit = "SL"; break
                if hi >= tp: hit = "TP"; break
            if hit:
                trades += 1
                if hit == "TP":
                    wins += 1
                    pnl = (tp/entry - 1.0)*100.0
                else:
                    pnl = (sl/entry - 1.0)*100.0
                pnls.append(pnl)
        # SELL sinyali
        elif c < e and ad > 20 and 32 <= r <= 50:
            entry = c; tp = entry - ATR_MULT * a; sl = entry + ATR_MULT * a
            hit = None
            for j in range(1, LOOK+1):
                hi = highs[i+j]; lo = lows[i+j]
                if hi >= sl: hit = "SL"; break
                if lo <= tp: hit = "TP"; break
            if hit:
                trades += 1
                if hit == "TP":
                    wins += 1
                    pnl = (1.0 - tp/entry)*100.0
                else:
                    pnl = (1.0 - sl/entry)*100.0
                pnls.append(pnl)

    if trades == 0:
        return 0, 0, 0.0
    avg_pnl = float(np.nanmean(pnls)) if pnls else 0.0
    return trades, wins, avg_pnl


async def backtest_coin(ex, symbol: str, bars: int):
    """
    Coin'i verilen TF'lerde test eder. Sonuç: total_trades, winrate, avg_pnl
    """
    total_trades = 0
    total_wins = 0
    pnl_list = []
    for tf in TIMEFRAMES:
        df = await fetch_ohlcv_full(ex, symbol, tf, bars)
        if df.empty:
            continue
        t, w, a = simple_signals_and_pnl(df, tf)
        total_trades += t
        total_wins += w
        if not math.isnan(a):
            pnl_list.append(a)
    if total_trades == 0:
        return {"symbol": symbol, "trades": 0, "winrate": 0.0, "avg_pnl": 0.0}
    winrate = (total_wins / total_trades) * 100.0
    avg_pnl = float(np.nanmean(pnl_list)) if pnl_list else 0.0
    return {"symbol": symbol, "trades": total_trades, "winrate": winrate, "avg_pnl": avg_pnl}


# =====================  U N I V E R S E   &   L I S T E L E R =====================

def norm_symbol_binance(sym: str) -> str:
    if sym.endswith("USDT"):
        return f"{sym[:-4]}/USDT"
    return sym

def fetch_binance_universe_sync(max_symbols=300):
    """
    Sync isteklerle Binance evreni (USDT spot) ve 24h hacme göre sıralama
    """
    try:
        exinfo = requests.get("https://api.binance.com/api/v3/exchangeInfo", timeout=10).json()
        symbols_meta = exinfo.get("symbols", [])
        usdt = []
        for s in symbols_meta:
            if s.get("status") != "TRADING":
                continue
            if s.get("quoteAsset") != "USDT":
                continue
            # spot izin kontrolü (esnek tutuyoruz)
            perms = s.get("permissions") or []
            if perms and ("SPOT" not in perms):
                continue
            usdt.append(s.get("symbol"))
        if not usdt:
            return []

        t24 = requests.get("https://api.binance.com/api/v3/ticker/24hr", timeout=10).json()
        vol_map = {}
        for it in t24:
            ss = it.get("symbol")
            if ss in usdt:
                try:
                    qv = float(it.get("quoteVolume", 0.0))
                except:
                    qv = 0.0
                vol_map[ss] = qv
        ranked = sorted(usdt, key=lambda x: vol_map.get(x, 0.0), reverse=True)
        ranked = ranked[:max_symbols]
        normed = [norm_symbol_binance(s) for s in ranked]
        return normed
    except Exception as e:
        logerr(e)
        return []

def load_active():
    return set(loadj(ACTIVE_FILE, []))

def save_active(active_set):
    savej(ACTIVE_FILE, sorted(list(active_set)))

def load_blacklist():
    return set(loadj(BLACKLIST_FILE, []))

def save_blacklist(black):
    savej(BLACKLIST_FILE, sorted(list(black)))

def append_scan_result(rec):
    data = loadj(SCAN_FILE, [])
    data.append(rec)
    savej(SCAN_FILE, data)


# =====================  A N A   I S L E M   A K I S I =====================

async def try_fill_active_list():
    """
    - Universe'i indir veya dosyadan yükle.
    - Blacklist'i uygula.
    - Active < TARGET_ACTIVE ise sırayla adayları test et ve doldur.
    """
    send_tg(MAIN_BOT_TOKEN, MAIN_CHAT_ID, f"🚀 Başlatıldı: {tzs(nowu())}\nHedef aktif: {TARGET_ACTIVE}, TF: {', '.join(TIMEFRAMES)}, Bars: {DEFAULT_BARS}")

    # Universe
    universe = loadj(UNIVERSE_FILE, [])
    if not universe:
        universe = fetch_binance_universe_sync(300)
        if universe:
            savej(UNIVERSE_FILE, universe)
    if not universe:
        send_tg(MAIN_BOT_TOKEN, MAIN_CHAT_ID, "⚠️ Universe alınamadı, çıkıyorum.")
        return

    # Listeler
    active = load_active()
    black  = load_blacklist()

    # Blacklist'i evrenden çıkar, ayrıca aktifte yanlışlıkla varsa temizle
    universe = [u for u in universe if u not in black]
    active = {a for a in active if a not in black}

    # Zaten 25 ve üstüyse sadece bilgi ver
    if len(active) >= TARGET_ACTIVE:
        save_active(active)
        send_tg(MAIN_BOT_TOKEN, MAIN_CHAT_ID, f"ℹ️ Zaten {len(active)} aktif var. İşlem yok.")
        return

    # Aday kuyruğu (aktif ve blacklist dışında kalanlar)
    queue = [u for u in universe if u not in active and u not in black]
    if not queue:
        send_tg(MAIN_BOT_TOKEN, MAIN_CHAT_ID, "⚠️ Aday kalmadı (blacklist dışı). Universe güncellemesi gerekebilir.")
        return

    # CCXT exchange
    ex = ccxt.binance({"enableRateLimit": True, "timeout": 10000})

    added = []
    blacked = []
    try:
        # Batch halinde dene
        idx = 0
        while len(active) < TARGET_ACTIVE and idx < len(queue):
            wait_system_sync()
            current_batch = queue[idx: idx+BATCH_SIZE]
            idx += BATCH_SIZE

            tasks = [backtest_coin(ex, sym, DEFAULT_BARS) for sym in current_batch]
            results = await asyncio.gather(*tasks, return_exceptions=True)

            for res in results:
                if isinstance(res, Exception):
                    logerr(res)
                    continue
                # kayıt
                append_scan_result({
                    "time": nowu().isoformat(),
                    "symbol": res["symbol"],
                    "trades": res["trades"],
                    "winrate": round(res["winrate"],2),
                    "avg_pnl": round(res["avg_pnl"],3)
                })

                sym = res["symbol"]
                ok = (res["trades"] >= MIN_TRADES_TH and
                      res["winrate"] >= WINRATE_TH and
                      res["avg_pnl"] >= AVG_PNL_TH)

                if ok:
                    if sym not in active:
                        active.add(sym)
                        added.append(sym)
                else:
                    black.add(sym)
                    blacked.append(sym)

            save_active(active)
            save_blacklist(black)

            if added:
                send_tg(MAIN_BOT_TOKEN, MAIN_CHAT_ID, f"✅ Aktife eklenenler ({len(added)}): {', '.join(added)}")
                added = []
            if blacked:
                send_tg(MAIN_BOT_TOKEN, MAIN_CHAT_ID, f"⛔ Blacklist ({len(blacked)}): {', '.join(blacked)}")
                blacked = []

            # 25 olduysa çık
            if len(active) >= TARGET_ACTIVE:
                break

        # Son durum
        save_active(active)
        save_blacklist(black)

        send_tg(MAIN_BOT_TOKEN, MAIN_CHAT_ID,
                f"📌 Tamamlandı.\nAktif: {len(active)}/{TARGET_ACTIVE}\nBlacklist: {len(black)}\nSaat: {tzs(nowu())}")

        # Özet tablo (son 25-50 kayıt)
        last = loadj(SCAN_FILE, [])
        if last:
            tail = last[-min(50, len(last)):]
            good = [x for x in tail if x["symbol"] in active]
            bad  = [x for x in tail if x["symbol"] in black]
            send_tg(MAIN_BOT_TOKEN, MAIN_CHAT_ID,
                    f"ℹ️ Son tarama özeti: ✓{len(good)} uygun, ✗{len(bad)} uygun değil.")

    except Exception as e:
        logerr(e)
        send_tg(MAIN_BOT_TOKEN, MAIN_CHAT_ID, f"❌ Hata: {repr(e)}")
    finally:
        try:
            await ex.close()
        except:
            pass


# =====================  Ç A L I Ş T I R M A   N O K T A S I =====================

if __name__ == "__main__":
    # İlk kurulum için dosyaları oluştur
    if not os.path.exists(ACTIVE_FILE):
        save_active(set())
    if not os.path.exists(BLACKLIST_FILE):
        save_blacklist(set())
    if not os.path.exists(SCAN_FILE):
        savej(SCAN_FILE, [])
    if not os.path.exists(UNIVERSE_FILE):
        savej(UNIVERSE_FILE, [])

    # Telegram'a başlamayı bildir
    try:
        send_tg(MAIN_BOT_TOKEN, MAIN_CHAT_ID, "🟢 CRT Analyzer v3 PRO (Eleme Modu) başlıyor…")
    except Exception as e:
        logerr(e)

    # Çalıştır
    asyncio.run(try_fill_active_list())