#!/usr/bin/env python3 """ Automated Leverage Trader Runs short scanner + spot scanner, opens positions in the Leverage Challenge game, manages exits (TP/SL/trailing stop), and reports via Telegram. Zero AI tokens — systemd timer. """ import json import os import sys import time import urllib.request from datetime import datetime, timezone from pathlib import Path # Add parent to path for imports sys.path.insert(0, str(Path(__file__).parent.parent)) from leverage_game import ( ensure_default_game, get_game, get_portfolio, open_position, close_position, update_prices, get_trades, get_leaderboard ) from scripts.short_scanner import scan_coin, COINS as SHORT_COINS TELEGRAM_BOT_TOKEN = os.environ.get("TELEGRAM_BOT_TOKEN", "") TELEGRAM_CHAT_ID = os.environ.get("TELEGRAM_CHAT_ID", "6443752046") DATA_DIR = Path(__file__).parent.parent / "data" / "leverage-game" STATE_FILE = DATA_DIR / "trader_state.json" # Trading params MARGIN_PER_TRADE = 200 # $200 margin per position DEFAULT_LEVERAGE = 10 # 10x default MAX_OPEN_POSITIONS = 10 # Max simultaneous positions SHORT_SCORE_THRESHOLD = 50 # Min score to open short LONG_SCORE_THRESHOLD = 45 # Min score to open long TP_PCT = 5.0 # Take profit at 5% on margin (50% on notional at 10x) SL_PCT = -3.0 # Stop loss at -3% on margin (30% on notional at 10x) TRAILING_STOP_PCT = 2.0 # Trailing stop: close if drops 2% from peak def load_state(): if STATE_FILE.exists(): return json.loads(STATE_FILE.read_text()) return {"peak_pnl": {}, "last_alert": None} def save_state(state): STATE_FILE.parent.mkdir(parents=True, exist_ok=True) STATE_FILE.write_text(json.dumps(state, indent=2)) def send_telegram(message): if not TELEGRAM_BOT_TOKEN: print(f"[TG] {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 failed: {e}") def run_short_scan(): """Run short scanner on all coins.""" results = [] for symbol in SHORT_COINS: r = scan_coin(symbol) if r: r["direction"] = "short" results.append(r) time.sleep(0.15) return sorted(results, key=lambda x: x['score'], reverse=True) def run_spot_scan(): """Run spot/long scanner (inverse of short criteria — oversold = buy).""" results = [] for symbol in SHORT_COINS: r = scan_coin(symbol) if r: # Invert: low RSI + below VWAP = long opportunity long_score = 0 reasons = [] if r['rsi'] <= 25: long_score += 30 reasons.append(f"RSI extremely oversold ({r['rsi']})") elif r['rsi'] <= 30: long_score += 25 reasons.append(f"RSI oversold ({r['rsi']})") elif r['rsi'] <= 35: long_score += 15 reasons.append(f"RSI low ({r['rsi']})") elif r['rsi'] <= 40: long_score += 5 reasons.append(f"RSI mildly low ({r['rsi']})") if r['vwap_pct'] < -5: long_score += 20 reasons.append(f"Well below VWAP ({r['vwap_pct']:+.1f}%)") elif r['vwap_pct'] < -3: long_score += 15 reasons.append(f"Below VWAP ({r['vwap_pct']:+.1f}%)") elif r['vwap_pct'] < -1: long_score += 8 reasons.append(f"Slightly below VWAP ({r['vwap_pct']:+.1f}%)") if r['change_24h'] < -15: long_score += 15 reasons.append(f"Dumped {r['change_24h']:.1f}% 24h") elif r['change_24h'] < -8: long_score += 10 reasons.append(f"Down {r['change_24h']:.1f}% 24h") elif r['change_24h'] < -4: long_score += 5 reasons.append(f"Down {r['change_24h']:.1f}% 24h") if r['bb_position'] < 0: long_score += 15 reasons.append(f"Below lower Bollinger ({r['bb_position']:.2f})") elif r['bb_position'] < 0.15: long_score += 10 reasons.append(f"Near lower Bollinger ({r['bb_position']:.2f})") results.append({ "symbol": r["symbol"], "price": r["price"], "rsi": r["rsi"], "vwap_pct": r["vwap_pct"], "change_24h": r["change_24h"], "bb_position": r["bb_position"], "score": long_score, "reasons": reasons, "direction": "long", }) time.sleep(0.15) return sorted(results, key=lambda x: x['score'], reverse=True) def manage_exits(game_id, username, state): """Check open positions for TP/SL/trailing stop exits.""" pf = get_portfolio(game_id, username) if not pf: return [] exits = [] for pos_id, pos in list(pf["positions"].items()): pnl_pct = (pos.get("unrealized_pnl", 0) / pos["margin_usd"] * 100) if pos["margin_usd"] > 0 else 0 # Track peak PnL for trailing stop peak_key = pos_id if peak_key not in state.get("peak_pnl", {}): state["peak_pnl"][peak_key] = pnl_pct if pnl_pct > state["peak_pnl"].get(peak_key, 0): state["peak_pnl"][peak_key] = pnl_pct peak = state["peak_pnl"].get(peak_key, 0) reason = None # Take profit if pnl_pct >= TP_PCT: reason = f"TP hit ({pnl_pct:+.1f}%)" # Stop loss elif pnl_pct <= SL_PCT: reason = f"SL hit ({pnl_pct:+.1f}%)" # Trailing stop (only if we were profitable) elif peak >= 2.0 and (peak - pnl_pct) >= TRAILING_STOP_PCT: reason = f"Trailing stop (peak {peak:+.1f}%, now {pnl_pct:+.1f}%)" if reason: result = close_position(game_id, username, pos_id, reason=reason) if result.get("success"): exits.append(result) # Clean up peak tracking state["peak_pnl"].pop(peak_key, None) return exits def main(): game_id = ensure_default_game() state = load_state() print(f"=== Leverage Trader ===") print(f"Time: {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M UTC')}") print(f"Game: {game_id}") # 1. Update prices and check liquidations liquidations = update_prices(game_id, "case") for liq in liquidations: msg = f"💀 LIQUIDATED: {liq['symbol']} {liq['direction']} {liq.get('leverage', '?')}x | Lost ${abs(liq.get('pnl', 0)):.2f}" send_telegram(msg) print(msg) # 2. Manage exits (TP/SL/trailing) exits = manage_exits(game_id, "case", state) for ex in exits: emoji = "✅" if ex.get("pnl", 0) > 0 else "❌" msg = (f"{emoji} Closed: {ex['symbol']} {ex['direction']} | " f"Entry: ${ex['entry_price']:.4f} → Exit: ${ex['exit_price']:.4f} | " f"PnL: ${ex['pnl']:+.2f} ({ex['pnl_pct']:+.1f}%)") print(msg) # 3. Get current portfolio pf = get_portfolio(game_id, "case") num_open = pf["num_positions"] if pf else 0 slots = MAX_OPEN_POSITIONS - num_open print(f"\nPortfolio: ${pf['equity']:,.2f} ({pf['pnl_pct']:+.2f}%) | {num_open} positions | {slots} slots open") # 4. Scan for new opportunities if slots > 0: # Run both scanners shorts = run_short_scan() longs = run_spot_scan() # Get existing symbols to avoid doubling up existing_symbols = set() if pf: for pos in pf["positions"].values(): existing_symbols.add(pos["symbol"]) opened = [] # Open short positions for r in shorts: if slots <= 0: break if r["score"] < SHORT_SCORE_THRESHOLD: break if r["symbol"] in existing_symbols: continue lev = 15 if r["score"] >= 70 else 10 if r["score"] >= 60 else 7 result = open_position(game_id, "case", r["symbol"], "short", MARGIN_PER_TRADE, lev, reason=f"Short scanner score:{r['score']}") if result.get("success"): opened.append(result) existing_symbols.add(r["symbol"]) slots -= 1 time.sleep(0.2) # Open long positions for r in longs: if slots <= 0: break if r["score"] < LONG_SCORE_THRESHOLD: break if r["symbol"] in existing_symbols: continue lev = 15 if r["score"] >= 70 else 10 if r["score"] >= 60 else 7 result = open_position(game_id, "case", r["symbol"], "long", MARGIN_PER_TRADE, lev, reason=f"Long scanner score:{r['score']}") if result.get("success"): opened.append(result) existing_symbols.add(r["symbol"]) slots -= 1 time.sleep(0.2) if opened: lines = [f"📊 Opened {len(opened)} positions\n"] for o in opened: lines.append(f"{'🔴' if o['direction']=='short' else '🟢'} {o['symbol']} {o['direction']} {o['leverage']}x @ ${o['entry_price']:.4f} (${o['margin']:.0f} margin)") send_telegram("\n".join(lines)) print(f"\nOpened {len(opened)} new positions") # 5. Send periodic summary (every 4 hours) if exits or liquidations: pf = get_portfolio(game_id, "case") # Refresh msg = (f"📈 Leverage Challenge Update\n" f"Equity: ${pf['equity']:,.2f} ({pf['pnl_pct']:+.2f}%)\n" f"Positions: {pf['num_positions']} | Cash: ${pf['cash']:,.2f}\n" f"Realized PnL: ${pf['realized_pnl']:+,.2f} | Fees: ${pf['total_fees']:,.2f}") send_telegram(msg) save_state(state) print("\nDone.") if __name__ == "__main__": main()