← Back
"""
Gerchik Scanner — Instrument filter + level scan loop.

Scans top futures pairs, finds levels on 1H, detects patterns on 15m.
"""

import asyncio
import logging
from dataclasses import dataclass
from typing import Callable, Optional

from binance.client import Client

from gerchik_config import (
    GERCHIK_MIN_VOLUME_24H,
    GERCHIK_MIN_ATR_PCT,
    GERCHIK_TOP_N_PAIRS,
    GERCHIK_LEVEL_CANDLES,
    GERCHIK_SL_BUFFER_PCT,
    GERCHIK_TP1_RR,
    GERCHIK_TP2_RR,
    GERCHIK_TP3_RR,
    GERCHIK_MAX_SL_PCT,
)
from gerchik_levels import find_levels
from gerchik_models import detect_all_models, GerchikSignal

logger = logging.getLogger(__name__)

# Reuse from config.py
from config import SKIP_TICKERS

# Extra blacklist for Gerchik only (too volatile for level trading)
GERCHIK_EXTRA_SKIP = {"NOM", "1000NOM"}


def get_top_pairs(client: Client, top_n: int = GERCHIK_TOP_N_PAIRS) -> list[str]:
    """
    Get top N USDT perpetual futures pairs by 24h volume.
    Excludes SKIP_TICKERS.
    """
    try:
        tickers = client.futures_ticker()
        usdt_pairs = []

        for t in tickers:
            symbol = t["symbol"]
            if not symbol.endswith("USDT"):
                continue

            # Extract base ticker
            base = symbol.replace("USDT", "")
            if base in SKIP_TICKERS or base in GERCHIK_EXTRA_SKIP:
                continue

            volume_usdt = float(t.get("quoteVolume", 0))
            if volume_usdt < GERCHIK_MIN_VOLUME_24H:
                continue

            usdt_pairs.append({
                "symbol": symbol,
                "volume": volume_usdt,
                "price": float(t.get("lastPrice", 0)),
            })

        # Sort by volume descending
        usdt_pairs.sort(key=lambda x: x["volume"], reverse=True)
        result = [p["symbol"] for p in usdt_pairs[:top_n]]
        return result

    except Exception as e:
        logger.error(f"Failed to get top pairs: {e}")
        return []


def fetch_klines(client: Client, symbol: str, interval: str, limit: int) -> dict:
    """
    Fetch OHLCV klines from Binance.
    Returns dict with lists: opens, highs, lows, closes, volumes.
    Uses completed candles only (drops last incomplete candle).
    """
    try:
        raw = client.futures_klines(symbol=symbol, interval=interval, limit=limit + 1)

        # Drop last candle (incomplete)
        if len(raw) > 1:
            raw = raw[:-1]

        opens = [float(k[1]) for k in raw]
        highs = [float(k[2]) for k in raw]
        lows = [float(k[3]) for k in raw]
        closes = [float(k[4]) for k in raw]
        volumes = [float(k[5]) for k in raw]

        return {
            "opens": opens,
            "highs": highs,
            "lows": lows,
            "closes": closes,
            "volumes": volumes,
        }
    except Exception as e:
        logger.error(f"Failed to fetch klines {symbol} {interval}: {e}")
        return {"opens": [], "highs": [], "lows": [], "closes": [], "volumes": []}


def calc_atr(highs: list[float], lows: list[float], closes: list[float], period: int = 14) -> float:
    """Calculate ATR as percentage of current price."""
    if len(closes) < period + 1:
        return 0

    trs = []
    for i in range(1, len(closes)):
        tr = max(
            highs[i] - lows[i],
            abs(highs[i] - closes[i - 1]),
            abs(lows[i] - closes[i - 1]),
        )
        trs.append(tr)

    if len(trs) < period:
        return 0

    atr = sum(trs[-period:]) / period
    current_price = closes[-1]

    return (atr / current_price * 100) if current_price > 0 else 0


@dataclass
class NearbyLevel:
    """A level that price is approaching — for watchlist/alerts."""
    symbol: str
    level_price: float
    level_type: str
    level_strength: float
    level_touches: int
    distance_pct: float     # How far price is from level (%)
    current_price: float
    side: str               # Expected trade direction if bounce


# Threshold: price within this % of a level = "nearby"
NEARBY_THRESHOLD_PCT = 0.5


async def scan_for_gerchik_signals(
    client: Client,
    on_signal: Callable,
    skip_symbols: set,
) -> dict:
    """
    Full scan: get pairs → fetch candles → find levels → detect patterns.

    Args:
        client: Binance client
        on_signal: async callback for each signal found
        skip_symbols: symbols to skip (already have positions)

    Returns:
        dict with scan results: signals_found, checked, nearby_levels, levels_total
    """
    pairs = get_top_pairs(client)
    if not pairs:
        logger.warning("No pairs found for Gerchik scan")
        return {"signals_found": 0, "checked": 0, "nearby": [], "levels_total": 0}

    signals_found = 0
    checked = 0
    nearby_levels: list[NearbyLevel] = []
    total_levels = 0

    for symbol in pairs:
        if symbol in skip_symbols:
            continue

        try:
            # Fetch 1H candles for levels
            klines_1h = fetch_klines(client, symbol, "1h", GERCHIK_LEVEL_CANDLES)
            if len(klines_1h["closes"]) < 100:
                continue

            # Check ATR filter
            atr_pct = calc_atr(klines_1h["highs"], klines_1h["lows"], klines_1h["closes"])
            if atr_pct < GERCHIK_MIN_ATR_PCT:
                continue

            checked += 1

            current_price = klines_1h["closes"][-1]

            # Find levels on 1H
            levels = find_levels(
                klines_1h["opens"],
                klines_1h["highs"],
                klines_1h["lows"],
                klines_1h["closes"],
                current_price,
            )

            if not levels:
                continue

            total_levels += len(levels)

            # Track nearby levels (price approaching strong level)
            for level in levels[:5]:
                dist_pct = abs(level.price - current_price) / current_price * 100
                if dist_pct <= NEARBY_THRESHOLD_PCT and level.strength >= 40:
                    side = "BUY" if level.price < current_price else "SELL"
                    nearby_levels.append(NearbyLevel(
                        symbol=symbol,
                        level_price=level.price,
                        level_type=level.level_type,
                        level_strength=level.strength,
                        level_touches=level.touches,
                        distance_pct=round(dist_pct, 3),
                        current_price=current_price,
                        side=side,
                    ))

            # Fetch 15m candles for pattern detection
            klines_15m = fetch_klines(client, symbol, "15m", 100)
            if len(klines_15m["closes"]) < 20:
                continue

            # Try each level for pattern match
            for level in levels[:5]:  # Only check nearest 5 levels
                signal = detect_all_models(
                    symbol=symbol,
                    level=level,
                    opens_15m=klines_15m["opens"],
                    highs_15m=klines_15m["highs"],
                    lows_15m=klines_15m["lows"],
                    closes_15m=klines_15m["closes"],
                    volumes_15m=klines_15m["volumes"],
                    closes_1h=klines_1h["closes"],
                    current_price=current_price,
                    sl_buffer_pct=GERCHIK_SL_BUFFER_PCT,
                    tp1_rr=GERCHIK_TP1_RR,
                    tp2_rr=GERCHIK_TP2_RR,
                    tp3_rr=GERCHIK_TP3_RR,
                    max_sl_pct=GERCHIK_MAX_SL_PCT,
                )

                if signal:
                    await on_signal(signal)
                    signals_found += 1
                    break  # One signal per symbol max

            # Rate limit: don't hammer Binance
            await asyncio.sleep(0.1)

        except Exception as e:
            logger.error(f"Error scanning {symbol}: {e}", exc_info=True)
            continue

    # Sort nearby by strength (strongest first)
    nearby_levels.sort(key=lambda n: n.level_strength, reverse=True)

    logger.info(
        f"[gerchik_scanner] Scan: {checked} checked, "
        f"{total_levels} levels, {len(nearby_levels)} nearby, "
        f"{signals_found} signals"
    )

    return {
        "signals_found": signals_found,
        "checked": checked,
        "nearby": nearby_levels[:10],  # Top 10 nearest
        "levels_total": total_levels,
    }


def scan_levels_for_symbol(client: Client, symbol: str) -> list:
    """
    Find levels for a single symbol (for /levels command).
    Returns list of Level objects.
    """
    klines = fetch_klines(client, symbol, "1h", GERCHIK_LEVEL_CANDLES)
    if len(klines["closes"]) < 100:
        return []

    current_price = klines["closes"][-1]
    return find_levels(
        klines["opens"], klines["highs"], klines["lows"], klines["closes"],
        current_price,
    )

📜 Git History

c6f6bd5chore: initial commit — version control setup5 weeks ago
Show last diff
Loading...