#!/usr/bin/env python3 """ Crypto Short Signal Scanner Scans for overbought coins ripe for shorting. Criteria: high RSI, price above VWAP, fading momentum, bearish divergence. Zero AI tokens — runs as pure Python via systemd timer. """ import json import os import sys import time import math import urllib.request from datetime import datetime, timezone, timedelta from pathlib import Path # Config DATA_DIR = Path(__file__).parent.parent / "data" / "short-scanner" DATA_DIR.mkdir(parents=True, exist_ok=True) SCAN_LOG = DATA_DIR / "scan_log.json" TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "") TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "6443752046") BINANCE_KLINES = "https://api.binance.us/api/v3/klines" BINANCE_TICKER = "https://api.binance.us/api/v3/ticker/24hr" # Coins to scan (popular leveraged trading coins) COINS = [ "BTCUSDT", "ETHUSDT", "SOLUSDT", "XRPUSDT", "DOGEUSDT", "ADAUSDT", "AVAXUSDT", "LINKUSDT", "DOTUSDT", "MATICUSDT", "NEARUSDT", "ATOMUSDT", "LTCUSDT", "UNIUSDT", "AAVEUSDT", "FILUSDT", "ALGOUSDT", "XLMUSDT", "VETUSDT", "ICPUSDT", "APTUSDT", "SUIUSDT", "ARBUSDT", "OPUSDT", "SEIUSDT", "HYPEUSDT", "TRUMPUSDT", "PUMPUSDT", "ASTERUSDT", ] def get_klines(symbol, interval='1h', limit=100): """Fetch klines from Binance US.""" url = f"{BINANCE_KLINES}?symbol={symbol}&interval={interval}&limit={limit}" req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) try: resp = urllib.request.urlopen(req, timeout=10) raw = json.loads(resp.read()) return [{ 'open': float(k[1]), 'high': float(k[2]), 'low': float(k[3]), 'close': float(k[4]), 'volume': float(k[5]), 'close_time': k[6], } for k in raw] except: return [] def calc_rsi(closes, period=14): """Calculate RSI.""" if len(closes) < period + 1: return 50 deltas = [closes[i] - closes[i-1] for i in range(1, len(closes))] gains = [d if d > 0 else 0 for d in deltas] losses = [-d if d < 0 else 0 for d in deltas] avg_gain = sum(gains[:period]) / period avg_loss = sum(losses[:period]) / period for i in range(period, len(deltas)): avg_gain = (avg_gain * (period - 1) + gains[i]) / period avg_loss = (avg_loss * (period - 1) + losses[i]) / period if avg_loss == 0: return 100 rs = avg_gain / avg_loss return round(100 - (100 / (1 + rs)), 1) def calc_vwap(klines): """Calculate VWAP from klines.""" cum_vol = 0 cum_tp_vol = 0 for k in klines: tp = (k['high'] + k['low'] + k['close']) / 3 cum_vol += k['volume'] cum_tp_vol += tp * k['volume'] if cum_vol == 0: return 0 return cum_tp_vol / cum_vol def calc_ema(values, period): """Calculate EMA.""" if not values: return 0 multiplier = 2 / (period + 1) ema = values[0] for v in values[1:]: ema = (v - ema) * multiplier + ema return ema def calc_macd(closes): """Calculate MACD (12, 26, 9).""" if len(closes) < 26: return 0, 0, 0 ema12 = calc_ema(closes, 12) ema26 = calc_ema(closes, 26) macd_line = ema12 - ema26 # Approximate signal line signal = calc_ema(closes[-9:], 9) if len(closes) >= 9 else macd_line histogram = macd_line - signal return macd_line, signal, histogram def calc_bollinger_position(closes, period=20): """How far price is from upper Bollinger band. >1 = above upper band.""" if len(closes) < period: return 0.5 recent = closes[-period:] sma = sum(recent) / period std = (sum((x - sma)**2 for x in recent) / period) ** 0.5 if std == 0: return 0.5 upper = sma + 2 * std lower = sma - 2 * std band_width = upper - lower if band_width == 0: return 0.5 return (closes[-1] - lower) / band_width def volume_trend(klines, lookback=10): """Compare recent volume to average. >1 means increasing volume.""" if len(klines) < lookback * 2: return 1.0 recent_vol = sum(k['volume'] for k in klines[-lookback:]) / lookback older_vol = sum(k['volume'] for k in klines[-lookback*2:-lookback]) / lookback if older_vol == 0: return 1.0 return recent_vol / older_vol def scan_coin(symbol): """Analyze a single coin for short signals.""" # Get 1h candles for RSI/VWAP/indicators klines_1h = get_klines(symbol, '1h', 100) if len(klines_1h) < 30: return None closes = [k['close'] for k in klines_1h] current_price = closes[-1] # RSI (14-period on 1h) rsi = calc_rsi(closes) # VWAP (24h) vwap_24h = calc_vwap(klines_1h[-24:]) vwap_pct = ((current_price - vwap_24h) / vwap_24h * 100) if vwap_24h else 0 # MACD macd_line, signal_line, histogram = calc_macd(closes) macd_bearish = histogram < 0 # Below signal = bearish # Bollinger position bb_pos = calc_bollinger_position(closes) # Volume trend vol_trend = volume_trend(klines_1h) # 24h change price_24h_ago = closes[-24] if len(closes) >= 24 else closes[0] change_24h = ((current_price - price_24h_ago) / price_24h_ago * 100) if price_24h_ago else 0 # 4h change (momentum) price_4h_ago = closes[-4] if len(closes) >= 4 else closes[0] change_4h = ((current_price - price_4h_ago) / price_4h_ago * 100) if price_4h_ago else 0 # === SHORT SCORING === score = 0 reasons = [] # RSI overbought (max 30 pts) if rsi >= 80: score += 30 reasons.append(f"RSI extremely overbought ({rsi})") elif rsi >= 70: score += 25 reasons.append(f"RSI overbought ({rsi})") elif rsi >= 65: score += 15 reasons.append(f"RSI elevated ({rsi})") elif rsi >= 60: score += 5 reasons.append(f"RSI mildly elevated ({rsi})") # Price above VWAP (max 20 pts) if vwap_pct > 5: score += 20 reasons.append(f"Well above VWAP (+{vwap_pct:.1f}%)") elif vwap_pct > 3: score += 15 reasons.append(f"Above VWAP (+{vwap_pct:.1f}%)") elif vwap_pct > 1: score += 8 reasons.append(f"Slightly above VWAP (+{vwap_pct:.1f}%)") # MACD bearish crossover (max 15 pts) if macd_bearish and histogram < -0.001 * current_price: score += 15 reasons.append("MACD bearish + accelerating") elif macd_bearish: score += 10 reasons.append("MACD bearish crossover") # Bollinger band position (max 15 pts) if bb_pos > 1.0: score += 15 reasons.append(f"Above upper Bollinger ({bb_pos:.2f})") elif bb_pos > 0.85: score += 10 reasons.append(f"Near upper Bollinger ({bb_pos:.2f})") # Big recent pump (mean reversion candidate) (max 15 pts) if change_24h > 15: score += 15 reasons.append(f"Pumped +{change_24h:.1f}% 24h") elif change_24h > 8: score += 10 reasons.append(f"Up +{change_24h:.1f}% 24h") elif change_24h > 4: score += 5 reasons.append(f"Up +{change_24h:.1f}% 24h") # Volume fading on uptrend (exhaustion) (5 pts) if change_24h > 2 and vol_trend < 0.7: score += 5 reasons.append("Volume fading on uptrend (exhaustion)") return { "symbol": symbol.replace("USDT", ""), "price": current_price, "rsi": rsi, "vwap_pct": round(vwap_pct, 2), "macd_histogram": round(histogram, 6), "bb_position": round(bb_pos, 2), "change_24h": round(change_24h, 2), "change_4h": round(change_4h, 2), "vol_trend": round(vol_trend, 2), "score": score, "reasons": reasons, "timestamp": datetime.now(timezone.utc).isoformat(), } def send_telegram_alert(message): """Send alert via Telegram bot API.""" if not TELEGRAM_BOT_TOKEN: print(f"[ALERT] {message}") return url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage" data = json.dumps({ "chat_id": TELEGRAM_CHAT_ID, "text": message, "parse_mode": "HTML" }).encode() req = urllib.request.Request(url, data=data, headers={ "Content-Type": "application/json", "User-Agent": "Mozilla/5.0" }) try: urllib.request.urlopen(req, timeout=10) except Exception as e: print(f"Telegram alert failed: {e}") def main(): print(f"=== Crypto Short Signal Scanner ===") print(f"Time: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}") print() results = [] for symbol in COINS: result = scan_coin(symbol) if result: results.append(result) time.sleep(0.15) # Rate limiting # Sort by score descending results.sort(key=lambda x: x['score'], reverse=True) # Print all results for r in results: emoji = "šŸ”“" if r['score'] >= 50 else "🟔" if r['score'] >= 30 else "⚪" print(f"{emoji} {r['symbol']:8s} score:{r['score']:3d} | RSI:{r['rsi']:5.1f} | VWAP:{r['vwap_pct']:+6.1f}% | 24h:{r['change_24h']:+6.1f}% | BB:{r['bb_position']:.2f}") if r['reasons']: for reason in r['reasons']: print(f" → {reason}") # Alert on strong short signals (score >= 50) strong = [r for r in results if r['score'] >= 50] if strong: lines = ["šŸ”“ Short Signals Detected\n"] for r in strong: lines.append(f"{r['symbol']} (score: {r['score']})") lines.append(f" Price: ${r['price']:.4f} | RSI: {r['rsi']} | VWAP: {r['vwap_pct']:+.1f}%") lines.append(f" 24h: {r['change_24h']:+.1f}% | BB: {r['bb_position']:.2f}") for reason in r['reasons']: lines.append(f" → {reason}") lines.append("") send_telegram_alert("\n".join(lines)) # Save scan log log = [] if SCAN_LOG.exists(): try: log = json.loads(SCAN_LOG.read_text()) except: pass log.append({ "timestamp": datetime.now(timezone.utc).isoformat(), "coins_scanned": len(results), "strong_signals": len(strong), "results": results, }) log = log[-500:] SCAN_LOG.write_text(json.dumps(log, indent=2)) print(f"\nšŸ“Š Summary: {len(results)} scanned, {len(strong)} strong short signals") if __name__ == "__main__": main()