← Назад
""" 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, )