Add short signal scanner + leverage trading game engine + auto-trader

- short_scanner.py: RSI/VWAP/MACD/Bollinger-based short signal detection
- leverage_game.py: Full game engine with longs/shorts/leverage/liquidations
- leverage_trader.py: Auto-trader connecting scanners to game with TP/SL/trailing stops
- Leverage Challenge game initialized: $10K, 20x max leverage, player 'case'
- systemd timer: every 15min scan + trade
- Telegram alerts on opens/closes/liquidations
This commit is contained in:
2026-02-09 20:32:18 -06:00
parent ab7abc2ea5
commit c5b941b487
11 changed files with 4362 additions and 1 deletions

View File

@ -0,0 +1,336 @@
#!/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 = ["🔴 <b>Short Signals Detected</b>\n"]
for r in strong:
lines.append(f"<b>{r['symbol']}</b> (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()