β ΠΠ°Π·Π°Π΄"""
Squeeze-VWAP Bot β Main Entry Point
=======================================
ΠΠ»Π°Π²Π½ΡΠΉ ΡΠΈΠΊΠ»:
1. Nuclear cleanup ΠΏΡΠΈ ΡΡΠ°ΡΡΠ΅
2. Recovery ΠΏΠΎΠ·ΠΈΡΠΈΠΉ (Squeeze + Zatochki)
3. Squeeze: PAUSED (6 Apr 2026 β replaced by Zatochki)
4. Zatochki: ΠΊΠ°ΠΆΠ΄ΡΠ΅ 60 ΡΠ΅ΠΊ β ΡΠΊΠ°Π½ + entry
5. ΠΠ°ΠΆΠ΄ΡΠ΅ 5 ΡΠ΅ΠΊ β ΠΌΠΎΠ½ΠΈΡΠΎΡΠΈΠ½Π³ ΠΏΠΎΠ·ΠΈΡΠΈΠΉ (both strategies)
Π‘ΡΡΠ°ΡΠ΅Π³ΠΈΠΈ:
- Squeeze Momentum + Z-VWAP + Waddah Attar + ADX (PAUSED)
- Zatochki (Knife Catcher) β Volume exhaustion reversal 1m (ACTIVE)
"""
import asyncio
import logging
import sys
import os
import time
# Add project root to path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from src.config import SCAN_INTERVAL_SEC, DATA_DIR
from src.zatochki_config import SCAN_INTERVAL_SEC as Z_SCAN_INTERVAL_SEC
from src.exchange import Exchange
from src.screener import Screener
from src.manager import TradeManager
from src.zatochki_screener import ZatochkiScreener
from src.zatochki_manager import ZatochkiManager
from src.bot import TelegramBot
from src.tmm_client import TMMClient
# Squeeze ON/OFF
SQUEEZE_ENABLED = False # PAUSED 6 Apr 2026 β Zatochki replaces it
ZATOCHKI_ENABLED = True
# ============================================================
# LOGGING
# ============================================================
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(name)s] %(levelname)s: %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
logger = logging.getLogger("main")
# Suppress noisy libs
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("telegram").setLevel(logging.WARNING)
# ============================================================
# MAIN
# ============================================================
async def main():
logger.info("=" * 50)
logger.info("Squeeze-VWAP Bot starting...")
logger.info(f"Squeeze: {'ACTIVE' if SQUEEZE_ENABLED else 'PAUSED'}")
logger.info(f"Zatochki: {'ACTIVE' if ZATOCHKI_ENABLED else 'PAUSED'}")
logger.info("=" * 50)
# Ensure data dir exists
os.makedirs(DATA_DIR, exist_ok=True)
# Init components
exchange = Exchange()
notify_queue = asyncio.Queue()
def sync_notify(msg):
"""Sync wrapper β ΠΊΠ»Π°Π΄ΡΡ ΡΠΎΠΎΠ±ΡΠ΅Π½ΠΈΠ΅ Π² ΠΎΡΠ΅ΡΠ΅Π΄Ρ Π΄Π»Ρ async ΠΎΡΠΏΡΠ°Π²ΠΊΠΈ."""
try:
notify_queue.put_nowait(msg)
except Exception:
pass
tmm = TMMClient()
# Squeeze components (even if paused β needed for recovery)
screener = Screener(exchange, notifier=sync_notify)
manager = TradeManager(exchange, screener, notifier=sync_notify, tmm=tmm)
screener.get_open_positions = lambda: manager.positions
# Zatochki components
z_screener = ZatochkiScreener(exchange, notifier=sync_notify)
z_manager = ZatochkiManager(exchange, z_screener, notifier=sync_notify, tmm=tmm)
bot = TelegramBot(screener=screener, exchange=exchange, tmm=tmm)
bot.manager = manager
bot.z_manager = z_manager # for /zstatus, /zstats commands
# Start Telegram bot
await bot.start()
logger.info("Telegram bot started")
# Startup notification
balance = exchange.get_balance()
squeeze_status = "ACTIVE" if SQUEEZE_ENABLED else "PAUSED"
zatochki_status = "ACTIVE" if ZATOCHKI_ENABLED else "PAUSED"
await bot.send_message(
f"\U0001f680 *Bot started*\n"
f"Balance: ${balance:.2f}\n"
f"Squeeze: {squeeze_status} ({len(manager.positions)} pos)\n"
f"\U0001f52a Zatochki: {zatochki_status} ({len(z_manager.positions)} pos)\n"
f"$5\\*10x | SL cap 1\\.2% | TP1 0\\.7%"
)
# Nuclear cleanup (only if no open positions from either strategy)
if not manager.positions and not z_manager.positions:
logger.info("Nuclear cleanup...")
exchange.nuclear_cleanup()
else:
logger.info("Skipping nuclear cleanup (open positions exist)")
# Recovery
logger.info("Recovery check (Squeeze)...")
manager.recovery()
logger.info("Recovery check (Zatochki)...")
z_manager.recovery()
# Main loop
last_squeeze_scan = 0
last_zatochki_scan = 0
check_interval = 5 # seconds between position checks
logger.info("Entering main loop...")
try:
while True:
now = time.time()
# === SQUEEZE SCAN (ΠΊΠ°ΠΆΠ΄ΡΠ΅ 5 ΠΌΠΈΠ½) β PAUSED ===
if SQUEEZE_ENABLED and now - last_squeeze_scan >= SCAN_INTERVAL_SEC:
try:
screener.run_scan()
last_squeeze_scan = now
except Exception as e:
logger.error(f"Squeeze scan error: {e}")
last_squeeze_scan = now
# === SQUEEZE ENTRIES ===
if SQUEEZE_ENABLED:
try:
manager.check_watchlist_for_entries()
except Exception as e:
logger.error(f"Squeeze entry error: {e}")
# === SQUEEZE MONITOR (always β might have recovery positions) ===
if manager.positions:
try:
manager.check_positions()
except Exception as e:
logger.error(f"Squeeze position error: {e}")
# === ZATOCHKI SCAN (ΠΊΠ°ΠΆΠ΄ΡΠ΅ 60 ΡΠ΅ΠΊ) ===
if ZATOCHKI_ENABLED and now - last_zatochki_scan >= Z_SCAN_INTERVAL_SEC:
try:
signals = z_screener.run_scan()
if signals:
z_manager.process_signals(signals)
last_zatochki_scan = now
except Exception as e:
logger.error(f"Zatochki scan error: {e}")
last_zatochki_scan = now
# === ZATOCHKI MONITOR ===
if z_manager.positions:
try:
z_manager.check_positions()
except Exception as e:
logger.error(f"Zatochki position error: {e}")
# === TMM RETRY PENDING TAGS ===
try:
tmm.retry_pending_tags()
except Exception:
pass
# === PROCESS NOTIFY QUEUE ===
while not notify_queue.empty():
try:
msg = notify_queue.get_nowait()
await bot.send_message(msg)
except Exception:
break
# Sleep
await asyncio.sleep(check_interval)
except KeyboardInterrupt:
logger.info("Shutting down...")
except Exception as e:
logger.error(f"Fatal error: {e}", exc_info=True)
await bot.send_message(f"\U0001f480 *Bot crashed:* {str(e)[:200]}")
finally:
await bot.stop()
logger.info("Bot stopped")
if __name__ == "__main__":
asyncio.run(main())