# Change this:
self.autocast = torch.cuda.amp.autocast(dtype=torch.float16)
# To this:
self.autocast = torch.amp.autocast('cuda', dtype=torch.float16)

# Change this:
self.scaler = torch.cuda.amp.GradScaler(enabled=self.use_fp16)
# To this:
self.scaler = torch.amp.GradScaler(enabled=self.use_fp16)             #!/usr/bin/env python3
"""
High-Performance Trading Bot for Solana
Utilizes GPU acceleration with FP16, PyTorch ML, and Rust for maximum performance.
Optimized for RTX 4070 Ti.
"""

import sys
import threading
import queue
import json
import base64
import time
import os
import signal
import traceback
import asyncio
import numpy as np
import torch
from pathlib import Path
from typing import Dict, List, Optional, Tuple, Any, Union
from datetime import datetime, timedelta
from collections import deque

# Import our custom modules
from solana_logger import logger
from config_loader import config
import ml_model

# Try to import the Rust module
try:
    import solana_rust_bot
    from solana_rust_bot import RaydiumTradingCore, TradingConfig, SwapParams, TokenPairSetting, TransactionStatus
    RUST_AVAILABLE = True
except ImportError as e:
    logger.error(f"Failed to import solana_rust_bot module: {e}")
    logger.warning("Running in simulation mode only.")
    RUST_AVAILABLE = False

# Message queue for communication between threads
message_queue = queue.Queue()

# Global event loop for async operations in sync context
EVENT_LOOP = None

# Balance cache to prevent excessive RPC calls
BALANCE_CACHE = {
    "sol": 0.0,
    "wsol": 0.0,
    "last_update": 0,
    "update_interval": 60,  # Update every 60 seconds
    "pending_transaction": False
}

class MLSignalTracker:
    """Track ML signals for trade decisions"""
    
    def __init__(self):
        self.signals = {
            "market_volatility": 0.35,
            "execution_confidence": 0.85,
            "price_direction": 0.2,
            "trade_profitability": 0.75,
            "risk_score": 0.3,
            "last_profit": 0.0,
            "execution_time": 0.0,
            "trade_count": 0
        }
    
    def update(self, key: str, value: float) -> None:
        """Update ML signal"""
        self.signals[key] = value
    
    def get(self, key: str, default_value: float = 0.0) -> float:
        """Get ML signal value"""
        return self.signals.get(key, default_value)
    
    def update_batch(self, updates: Dict[str, float]) -> None:
        """Update multiple signals at once"""
        self.signals.update(updates)


def run_async_in_thread(coro_func, *args, **kwargs):
    """
    Helper function to run an async function in the global event loop.
    This is needed to call async Rust functions from synchronous code.
    
    IMPORTANT: Pass the coroutine function and its arguments separately,
    not the awaitable coroutine object itself, to prevent reuse errors.
    """
    global EVENT_LOOP
    if EVENT_LOOP is None or EVENT_LOOP.is_closed():
        # Create a new event loop if none exists or if it's closed
        EVENT_LOOP = asyncio.new_event_loop()
    
    timeout = kwargs.pop('timeout', 10)
    max_retries = 3
    retry_delay = 1.0
    
    for attempt in range(max_retries):
        try:
            # Create a future to get the result
            future = asyncio.run_coroutine_threadsafe(coro_func(*args, **kwargs), EVENT_LOOP)
            
            # Wait for the result with timeout and return it
            return future.result(timeout=timeout)
        except Exception as e:
            if "429 Too Many Requests" in str(e):
                # Rate limiting - use exponential backoff
                wait_time = retry_delay * (2 ** attempt)
                logger.warning(f"Rate limited. Waiting {wait_time:.1f}s before retry.")
                time.sleep(wait_time)
                continue
            elif attempt < max_retries - 1:
                # Other error, but we still have retries left
                logger.warning(f"Error in async call: {e}. Retrying...")
                time.sleep(retry_delay)
                continue
            else:
                # Out of retries
                raise


class TradingEngine:
    """
    High-Performance Trading Engine for Solana
    
    Key features:
    - FP16 GPU-accelerated trading for maximum performance on RTX GPUs
    - Thread-based architecture to avoid asyncio event loop issues
    - Uses PyTorch for ML-based decision making with mixed precision
    - Uses Rust for high-performance Solana interactions
    - Monitors transactions via WebSocket
    """
    
    def __init__(self):
        """Initialize the trading engine"""
        logger.info("Initializing High-Performance Trading Engine")
        
        # Initialize ML signal tracker
        self.ml_tracker = MLSignalTracker()
        
        # Initialize state variables
        self.is_running = False
        self.last_trade_time = 0
        self.opportunities_found = 0
        self.trades_executed = 0
        self.successful_trades = 0
        self.total_profit = 0.0
        self.start_time = time.time()
        
        # Performance tracking
        self.last_memory_check = time.time()
        
        # Trading statistics
        self.stats = {
            "opportunities": 0,
            "filtered_too_small": 0,
            "filtered_too_large": 0,
            "filtered_by_ml": 0,
            "filtered_by_time": 0,
            "viable": 0,
            "attempted": 0,
            "completed": 0,
            "profit": 0.0,
            "execution_times": [],
            "balance_history": [],
            "start_balance": 0.0,
            "current_balance": 0.0,
        }
        
        # Thread management
        self.threads = []
        self.event_loop_thread = None
        
        # Setup CUDA with FP16 optimization
        self.setup_cuda()
        
        # Start event loop thread first
        self.start_event_loop_thread()
        
        # Initialize the Rust trading core
        self.setup_trading_core()
        
        # Set up handling for graceful shutdown
        signal.signal(signal.SIGINT, self.handle_shutdown)
        signal.signal(signal.SIGTERM, self.handle_shutdown)
        
        # Load ML model
        self.model = self.load_ml_model()
        
        # Log initialization success
        logger.info("Trading Engine initialized successfully!")
    
    def start_event_loop_thread(self):
        """Start a thread that runs the event loop for async operations"""
        def run_event_loop():
            """Function to run in thread"""
            global EVENT_LOOP
            try:
                # Create new event loop for this thread
                EVENT_LOOP = asyncio.new_event_loop()
                asyncio.set_event_loop(EVENT_LOOP)
                logger.info("Event loop thread started")
                
                # Run the event loop forever
                EVENT_LOOP.run_forever()
            except Exception as e:
                logger.error(f"Event loop thread error: {e}")
                logger.error(traceback.format_exc())
            finally:
                if EVENT_LOOP and not EVENT_LOOP.is_closed():
                    EVENT_LOOP.close()
                logger.info("Event loop thread terminated")
        
        # Create and start the thread
        self.event_loop_thread = threading.Thread(target=run_event_loop, daemon=True)
        self.event_loop_thread.start()
        
        # Give it a moment to initialize
        time.sleep(0.5)
    
    def setup_cuda(self):
        """Configure CUDA for optimal performance with FP16 on RTX GPUs"""
        if not torch.cuda.is_available():
            logger.warning("CUDA not available! Using CPU for inference.")
            self.device = torch.device("cpu")
            self.use_fp16 = False
            return
        
        # Set device
        self.device = torch.device("cuda")
        
        # Check if Tensor Cores are available (RTX cards)
        self.has_tensor_cores = torch.cuda.get_device_capability()[0] >= 7  # Volta or newer
        
        # Enable TF32 acceleration if available (RTX 30 series or newer)
        self.use_tf32 = torch.cuda.get_device_capability()[0] >= 8  # Ampere or newer
        if self.use_tf32:
            torch.backends.cuda.matmul.allow_tf32 = config.get("ALLOW_TF32", True)
            torch.backends.cudnn.allow_tf32 = config.get("ALLOW_TF32", True)
            logger.info("TF32 acceleration enabled for tensor operations")
        
        # Enable FP16 for maximum performance
        self.use_fp16 = True
        logger.info("FP16 (half precision) enabled for maximum performance")
        
        # Set memory fraction
        memory_fraction = config.get("GPU_MEMORY_FRACTION", 0.95)
        torch.cuda.set_per_process_memory_fraction(memory_fraction)
        
        # Set stream priority (-1 is highest)
        stream_priority = config.get("STREAM_PRIORITY", -3)
        self.stream = torch.cuda.Stream(priority=stream_priority)
        
        # Enable cuDNN benchmark mode for optimized kernels
        torch.backends.cudnn.benchmark = config.get("CUDA_BENCHMARK", True)
        
        # Deterministic mode is off by default for speed
        torch.backends.cudnn.deterministic = config.get("CUDA_DETERMINISTIC", False)
        
        # CPU thread settings
        threads = config.get("THREADS", 48)
        torch.set_num_threads(threads)
        os.environ["MKL_NUM_THREADS"] = str(threads)
        os.environ["OMP_NUM_THREADS"] = str(threads)
        
        # Empty cache
        torch.cuda.empty_cache()
        
        # Set up autocast for mixed precision
        self.autocast = torch.cuda.amp.autocast(dtype=torch.float16)
        
        # Create scaler for mixed precision training (if needed)
        self.scaler = torch.cuda.amp.GradScaler(enabled=self.use_fp16)
        
        # Log GPU information
        gpu_name = torch.cuda.get_device_name(0)
        logger.info(f"Using GPU: {gpu_name}")
        logger.info(f"CUDA Version: {torch.version.cuda}")
        logger.info(f"Precision: {'FP16/Mixed' if self.use_fp16 else 'FP32'}")
        logger.info(f"Tensor Cores: {'Enabled' if self.has_tensor_cores else 'Not Available'}")
        logger.info(f"Allocated Memory: {torch.cuda.memory_allocated(0) / 1024**2:.2f} MB")
        logger.info(f"Reserved Memory: {torch.cuda.memory_reserved(0) / 1024**2:.2f} MB")
        
        # Warm up GPU (using FP16)
        self.warmup_gpu()
    
    def warmup_gpu(self):
        """Warm up GPU using FP16 to ensure consistent performance"""
        logger.info("Warming up GPU with FP16...")
        tensor_size = config.get("TENSOR_SIZE", 176)
        warmup_cycles = config.get("WARMUP_CYCLES", 2500)
        
        with torch.cuda.stream(self.stream):
            # Create FP16 tensors for warmup when supported
            dtype = torch.float16 if self.use_fp16 else torch.float32
            a = torch.randn(tensor_size, tensor_size, device=self.device, dtype=dtype)
            b = torch.randn(tensor_size, tensor_size, device=self.device, dtype=dtype)
            
            # Perform matrix multiplications to warm up the GPU
            start_time = time.time()
            for _ in range(warmup_cycles):
                if self.use_fp16:
                    with self.autocast:
                        _ = torch.matmul(a, b)
                else:
                    _ = torch.matmul(a, b)
            
            # Ensure all operations are completed
            torch.cuda.synchronize()
            
            elapsed = time.time() - start_time
            logger.info(f"GPU warmup completed in {elapsed:.2f} seconds, {warmup_cycles / elapsed:.2f} ops/s")
    
    def setup_trading_core(self):
        """Set up Rust trading core with PyO3 bindings"""
        try:
            if not RUST_AVAILABLE:
                logger.error("Rust module not available. Cannot continue.")
                sys.exit(1)
            
            # Create a base Trading Config to use
            rust_config = TradingConfig(
                rpc_url=config.get("RPC_URL"),
                ws_url=config.get("WS_URL"),
                api_key=config.get("API_KEY"),
                sol_mint=config.get("SOL_MINT"),
                rpc_rate_limit=config.get("RPC_RATE_LIMIT", 5),
                compute_limit=config.get("COMPUTE_LIMIT", 50000),
                priority_fee=config.get("PRIORITY_FEE", 750000),
                skip_preflight=config.get("SKIP_PREFLIGHT", True),
                max_retries=config.get("MAX_RETRIES", 3),
                front_run_amount=config.get("FRONT_RUN_AMOUNT", 0.35),
                min_victim_size=config.get("MIN_VICTIM_SIZE", 5.0),
                max_victim_size=config.get("MAX_VICTIM_SIZE", 320.0),
                min_profit=config.get("MIN_PROFIT", 0.002),
                slippage_bps=config.get("SLIPPAGE_BPS", 1000),
                min_sol_balance=config.get("MIN_SOL_BALANCE", 0.03),
                min_wsol_balance=config.get("MIN_WSOL_BALANCE", 0.02),
                raydium_program_id=config.get("RAYDIUM_PROGRAM_ID"),
                token_program_id=config.get("TOKEN_PROGRAM_ID"),
                system_program_id=config.get("SYSTEM_PROGRAM_ID"),
                associated_token_program_id=config.get("ASSOCIATED_TOKEN_PROGRAM_ID")
            )
            
            # CRITICAL FIX: Add SOL token pair setting first
            # This is needed for wrapping/unwrapping SOL
            sol_token_pair = TokenPairSetting(
                token_mint=config.get("SOL_MINT"),  # WSOL token mint
                amm_id=config.get("AMM_ID"),
                amm_authority=config.get("MARKET_AMM_AUTHORITY"),
                amm_open_orders=config.get("MARKET_AMM_OPEN_ORDERS"),
                amm_target=config.get("MARKET_AMM_TARGET"),
                pool_token_account=config.get("MARKET_POOL_TOKEN_ACCOUNT"),
                pool_pc_account=config.get("MARKET_POOL_PC_TOKEN_ACCOUNT"),
                min_liquidity_pct=95.0,
                min_exit_liquidity_sol=5.0,
                auth_revoked=True,
                buyer_seller_ratio=1.0,
                min_volume_5m=10.0,
                enabled=True
            )
            
            # Add SOL token pair to config
            rust_config.add_token_pair_setting(sol_token_pair)
            
            # Add BONK token pair setting
            bonk_token_pair = TokenPairSetting(
                token_mint=config.get("TOKEN_MINT"),  # BONK token mint
                amm_id=config.get("AMM_ID"),
                amm_authority=config.get("MARKET_AMM_AUTHORITY"),
                amm_open_orders=config.get("MARKET_AMM_OPEN_ORDERS"),
                amm_target=config.get("MARKET_AMM_TARGET"),
                pool_token_account=config.get("MARKET_POOL_TOKEN_ACCOUNT"),
                pool_pc_account=config.get("MARKET_POOL_PC_TOKEN_ACCOUNT"),
                min_liquidity_pct=95.0,
                min_exit_liquidity_sol=5.0,
                auth_revoked=True,
                buyer_seller_ratio=1.0,
                min_volume_5m=10.0,
                enabled=True
            )
            
            # Add BONK token pair to config
            rust_config.add_token_pair_setting(bonk_token_pair)
            
            # Get keypair from environment
            private_key = os.environ.get("SOLANA_PRIVATE_KEY")
            
            if private_key:
                logger.info("Using keypair from environment variable")
                self.trading_core = RaydiumTradingCore(
                    config.get("RPC_URL"),  # rpc_url
                    private_key,            # private_key (direct string)
                    None,                   # keypair_path (None since we're using direct key)
                    rust_config             # config
                )
            else:
                # Try to find a keypair file
                keypair_paths = RaydiumTradingCore.get_keypair_paths()
                keypair_path = None
                
                for path in keypair_paths:
                    if os.path.exists(path):
                        keypair_path = path
                        logger.info(f"Using keypair from: {path}")
                        break
                
                if keypair_path:
                    self.trading_core = RaydiumTradingCore(
                        config.get("RPC_URL"),  # rpc_url
                        None,                   # private_key (None since we're using path)
                        keypair_path,           # keypair_path
                        rust_config             # config
                    )
                else:
                    logger.error("No keypair found. Please set SOLANA_PRIVATE_KEY or provide a keypair file.")
                    sys.exit(1)
            
            # Verify we can get the public key
            public_key = self.trading_core.get_public_key()
            logger.info(f"Rust trading core initialized with public key: {public_key}")
            
            # Update initial ML signals
            self.update_ml_signals({
                "market_volatility": 0.35,
                "execution_confidence": 0.85,
                "price_direction": 0.2,
                "trade_profitability": 0.75,
                "risk_score": 0.3,
            })
            
            # Initialize balance history with current balance
            self.update_and_log_balances(force_update=True)
            
        except Exception as e:
            logger.error(f"Failed to initialize Rust trading core: {e}")
            logger.error(traceback.format_exc())
            sys.exit(1)
    
    def update_ml_signals(self, signals: Dict[str, float]):
        """Update ML signals in trading core"""
        for key, value in signals.items():
            # Update local tracker
            self.ml_tracker.update(key, value)
            
            # Also try to update in Rust if the method is available
            try:
                if hasattr(self.trading_core, "update_ml_signal"):
                    self.trading_core.update_ml_signal(key, value)
            except Exception as e:
                logger.debug(f"Could not update ML signal in Rust core: {e}")
    
    def initialize_wsol(self):
        """Initialize WSOL account if needed and wrap some SOL"""
        try:
            # Check if we have any WSOL
            sol_balance, wsol_balance = self._get_cached_balances(force_update=True)
            
            if wsol_balance > 0:
                logger.info(f"WSOL already initialized with {wsol_balance:.6f} WSOL")
                return
            
            logger.info("Initializing WSOL account and wrapping some SOL...")
            
            # Create WSOL ATA if it doesn't exist
            self.ensure_token_ata(config.get("SOL_MINT"))
            
            # Wrap a small amount of SOL for gas
            wrap_amount = min(sol_balance * 0.2, 0.1)  # 20% of SOL or max 0.1 SOL
            
            if wrap_amount > 0.01:  # Only wrap if amount is meaningful
                # Define the async function - NOT calling it yet
                async def wrap_sol_async():
                    params = SwapParams(
                        token_mint=config.get("SOL_MINT"),
                        amount_in=wrap_amount,
                        is_buy=True,
                        slippage_bps=config.get("SLIPPAGE_BPS", 1000)
                    )
                    return await self.trading_core.execute_swap(params)
                
                # Pass the function without calling it to avoid "already awaited" error
                result = run_async_in_thread(wrap_sol_async)
                
                if result.success:
                    logger.info(f"Wrapped {wrap_amount:.6f} SOL to WSOL. TX: {result.signature}")
                else:
                    logger.warning(f"Failed to wrap SOL: {result.error}")
                
                # Update balances
                self.update_and_log_balances(force_update=True)
            else:
                logger.warning(f"Not enough SOL to wrap ({sol_balance:.6f} SOL available)")
        
        except Exception as e:
            logger.error(f"Failed to initialize WSOL: {e}")
            logger.error(traceback.format_exc())
    
    def ensure_token_ata(self, token_mint):
        """Ensure Associated Token Account exists for a specific token"""
        try:
            # Define the async function - NOT calling it yet
            async def create_ata_async():
                return await self.trading_core.ensure_ata(token_mint)
            
            # Pass the function without calling it
            result = run_async_in_thread(create_ata_async)
            logger.info(f"Ensured ATA exists for token {token_mint}: {result}")
            return True
        
        except Exception as e:
            logger.error(f"Failed to create ATA for {token_mint}: {e}")
            logger.error(traceback.format_exc())
            return False
    
    # Memory and performance optimization
    def optimize_memory(self):
        """Perform memory optimization to prevent leaks during long runs"""
        # Clear PyTorch cache
        if torch.cuda.is_available():
            torch.cuda.empty_cache()
            
        # Suggest garbage collection
        import gc
        gc.collect()
        
        # Log memory usage
        if torch.cuda.is_available():
            allocated = torch.cuda.memory_allocated() / 1024**2
            reserved = torch.cuda.memory_reserved() / 1024**2
            logger.debug(f"GPU Memory - Allocated: {allocated:.2f} MB, Reserved: {reserved:.2f} MB")
        
        try:
            # CPU memory (if psutil is available)
            import psutil
            process = psutil.Process()
            memory_info = process.memory_info()
            logger.debug(f"CPU Memory - RSS: {memory_info.rss / 1024**2:.2f} MB, VMS: {memory_info.vms / 1024**2:.2f} MB")
        except ImportError:
            pass  # psutil not available
        
        self.last_memory_check = time.time()
    
    def _get_cached_balances(self, force_update=False):
        """
        Get SOL and WSOL balances from cache or update from blockchain
        This prevents excessive RPC calls
        """
        global BALANCE_CACHE
        
        # Check if we need to update the cache
        current_time = time.time()
        cache_age = current_time - BALANCE_CACHE["last_update"]
        
        if (force_update or 
            cache_age > BALANCE_CACHE["update_interval"] or
            BALANCE_CACHE["sol"] == 0.0):
            
            try:
                # Define async function to get balances
                async def get_balances_async():
                    return await self.trading_core.get_balances(None)  # Pass None to get all balances
                
                # Run in the event loop thread and wait for result
                sol_balance, token_balances = run_async_in_thread(get_balances_async)
                wsol_balance = token_balances.get(config.get("SOL_MINT"), 0.0)
                
                # Update cache
                BALANCE_CACHE["sol"] = sol_balance
                BALANCE_CACHE["wsol"] = wsol_balance
                BALANCE_CACHE["last_update"] = current_time
                
                return sol_balance, wsol_balance
                
            except Exception as e:
                if not force_update:  # Only log error if not forced update
                    logger.error(f"Error getting balances: {e}")
                return BALANCE_CACHE["sol"], BALANCE_CACHE["wsol"]
        else:
            # Return cached values
            return BALANCE_CACHE["sol"], BALANCE_CACHE["wsol"]
    
    def update_and_log_balances(self, force_update=False) -> Tuple[float, float]:
        """Update and log balances, return (sol_balance, wsol_balance)"""
        try:
            # Get balances (from cache unless forced)
            sol_balance, wsol_balance = self._get_cached_balances(force_update)
            
            # Update stats
            if self.stats["start_balance"] == 0.0:
                self.stats["start_balance"] = sol_balance
            
            self.stats["current_balance"] = sol_balance
            
            # Log balance only if forced or significant change
            if force_update or len(self.stats["balance_history"]) == 0:
                # Log balance
                self.stats["balance_history"].append({
                    "timestamp": time.time(),
                    "sol": sol_balance,
                    "wsol": wsol_balance,
                    "total": sol_balance + wsol_balance
                })
                
                # Calculate lifetime profit
                lifetime_profit = sol_balance - self.stats["start_balance"]
                
                logger.info(f"Balances - SOL: {sol_balance:.6f}, WSOL: {wsol_balance:.6f}")
                logger.info(f"Lifetime profit: {lifetime_profit:.6f} SOL")
            
            return sol_balance, wsol_balance
            
        except Exception as e:
            logger.error(f"Failed to update balances: {e}")
            logger.error(traceback.format_exc())
            return 0.0, 0.0
    
    def decode_ray_log(self, log_str: str) -> Optional[bytes]:
        """Decode Raydium log data from base64"""
        try:
            if "ray_log:" not in log_str:
                return None
            
            b64_data = log_str.split("ray_log:")[1].strip()
            raw_data = base64.b64decode(b64_data)
            return raw_data
        
        except Exception as e:
            logger.error(f"Error decoding ray log: {e}")
            return None
    
    def validate_trade(self, raw_data: bytes) -> Optional[Dict[str, Any]]:
        """Validate and extract trade information from raw Raydium log data"""
        try:
            # Basic validation: minimum length
            if len(raw_data) < 9:
                return None
            
            # Extract instruction and amount (first 9 bytes)
            instruction = raw_data[0]
            amount = int.from_bytes(raw_data[1:9], byteorder="little") / 1e9
            
            # Check if it's a buy (buy instructions in Raydium)
            # From test_solana_trading.py: buy_instructions = {0x03, 0x09}
            # Also include 11 (0x0b) which is the newer swap instruction
            is_buy = instruction in (3, 9, 11)
            
            # Skip if not a buy
            if not is_buy:
                return None
            
            # Additional validation
            min_size = config.get("MIN_VICTIM_SIZE", 5.0)
            max_size = config.get("MAX_VICTIM_SIZE", 320.0)
            
            if amount < min_size:
                self.stats["filtered_too_small"] += 1
                return None
            
            if amount > max_size:
                self.stats["filtered_too_large"] += 1
                return None
            
            # Return validated trade info
            return {
                "instruction": instruction,
                "amount": amount,
                "is_buy": is_buy,
                "raw_data": raw_data,
                "timestamp": time.time()
            }
        
        except Exception as e:
            logger.error(f"Error validating trade: {e}")
            return None
    
    def load_ml_model(self) -> Optional[torch.nn.Module]:
        """Load ML model for trade evaluation"""
        model_path = config.get("MODEL_PATH", "models/trade_model.pt")
        
        try:
            if Path(model_path).exists():
                logger.info(f"Loading ML model from {model_path}")
                
                # Use our ml_model module's load function
                model = ml_model.load_model(model_path)
                
                if model:
                    # Test with dummy input using FP16
                    with torch.cuda.stream(self.stream):
                        dummy_input = torch.randn(1, 32, device=self.device)
                        if self.use_fp16:
                            dummy_input = dummy_input.half()  # Convert to FP16
                            
                        with torch.no_grad():
                            with self.autocast:
                                _ = model(dummy_input)
                    
                    logger.info("ML model loaded and tested successfully")
                    return model
                else:
                    logger.warning("Failed to load ML model")
                    return None
            else:
                logger.warning(f"ML model not found at {model_path}. Running in fallback mode.")
                return None
        
        except Exception as e:
            logger.error(f"Failed to load ML model: {e}")
            return None
    
    def prepare_ml_features(self, trade_info: Dict[str, Any]) -> torch.Tensor:
        """Prepare features for ML model input (using FP16 when available)"""
        # Extract amount from trade info
        amount = trade_info["amount"]
        raw_data = trade_info["raw_data"]
        
        # Create base features
        base_features = [
            amount,  # Victim size
            config.get("FRONT_RUN_AMOUNT", 0.35),  # Our position size
            time.time() % 86400 / 86400,  # Time of day normalized to [0,1]
        ]
        
        # Get ML signals from our tracker
        signals = [
            self.ml_tracker.get("market_volatility", 0.5),
            self.ml_tracker.get("execution_confidence", 0.85),
            self.ml_tracker.get("price_direction", 0.0),
            self.ml_tracker.get("trade_profitability", 0.75),
            self.ml_tracker.get("risk_score", 0.3),
        ]
        
        # Add normalized bytes from raw data (first 20 bytes)
        data_features = [b / 255.0 for b in raw_data[:min(20, len(raw_data))]]
        
        # Combine all features
        features = base_features + signals + data_features
        
        # Pad or truncate to expected size (32)
        if len(features) < 32:
            features.extend([0.0] * (32 - len(features)))
        else:
            features = features[:32]
        
        # Convert to tensor with batch dimension
        tensor = torch.tensor([features], dtype=torch.float32, device=self.device)
        
        # Convert to half precision if using FP16
        if self.use_fp16:
            tensor = tensor.half()
            
        return tensor
    
    def predict_trade_success(self, features: torch.Tensor) -> float:
        """Predict trade success probability using ML model with FP16 optimization"""
        if self.model is None:
            # Fallback logic without ML model
            return 0.85  # Default confidence
        
        try:
            with torch.cuda.stream(self.stream):
                with torch.no_grad():
                    # Always use autocast when FP16 is enabled
                    if self.use_fp16:
                        with self.autocast:
                            output = self.model(features)
                    else:
                        output = self.model(features)
                    
                    # Get prediction (probability)
                    confidence = output.item()
                    return confidence
        
        except Exception as e:
            logger.error(f"Error during model inference: {e}")
            return 0.0  # Zero confidence on error
    
    def estimate_profit(self, amount: float) -> float:
        """Estimate potential profit based on victim trade size"""
        # Profit rates based on victim size
        if amount >= 200:
            profit_rate = 0.018  # 1.8% for large trades
        elif amount >= 100:
            profit_rate = 0.015  # 1.5% for medium-large trades
        elif amount >= 50:
            profit_rate = 0.012  # 1.2% for medium trades
        elif amount >= 10:
            profit_rate = 0.010  # 1.0% for small-medium trades
        else:
            profit_rate = 0.008  # 0.8% for small trades
        
        # Our position size
        position_size = config.get("FRONT_RUN_AMOUNT", 0.35)
        
        # Calculate estimated gross profit
        gross_profit = position_size * profit_rate
        
        # Subtract gas costs
        gas_cost = config.get("PRIORITY_FEE", 750000) / 1e9 * 2  # Both ways
        net_profit = gross_profit - gas_cost
        
        return net_profit
    
    def process_trade_opportunity(self, trade_info: Dict[str, Any]):
        """Process and execute a trade opportunity if profitable"""
        amount = trade_info["amount"]
        
        logger.info(f"Evaluating trade opportunity: {amount:.4f} SOL")
        
        # Track statistics
        self.stats["opportunities"] += 1
        
        # Check if we have a pending transaction
        global BALANCE_CACHE
        if BALANCE_CACHE["pending_transaction"]:
            logger.debug(f"Skipping: Transaction already in progress")
            return
        
        # Check balance is sufficient
        sol_balance, wsol_balance = self._get_cached_balances()
        
        if sol_balance < config.get("MIN_SOL_BALANCE", 0.03):
            logger.debug(f"Skipping: SOL balance too low ({sol_balance:.6f})")
            return
        
        # Estimate potential profit
        estimated_profit = self.estimate_profit(amount)
        min_profit = config.get("MIN_PROFIT", 0.002)
        
        if estimated_profit < min_profit:
            logger.debug(f"Skipping: Estimated profit ({estimated_profit:.6f} SOL) below threshold ({min_profit:.6f} SOL)")
            return
        
        # Check if we're in cooldown period
        cooldown_ms = config.get("COOLDOWN_AFTER_TRADE_MS", 200)
        elapsed_since_last_trade = (time.time() - self.last_trade_time) * 1000
        
        if elapsed_since_last_trade < cooldown_ms:
            logger.debug(f"Skipping: In cooldown period ({elapsed_since_last_trade:.0f}ms < {cooldown_ms}ms)")
            self.stats["filtered_by_time"] += 1
            return
        
        # Prepare features for ML prediction
        features = self.prepare_ml_features(trade_info)
        
        # Get ML prediction
        confidence = self.predict_trade_success(features)
        confidence_threshold = config.get("ML_CONFIDENCE_THRESHOLD", 0.7)
        
        logger.info(f"ML evaluation: Confidence={confidence:.4f}, Threshold={confidence_threshold:.2f}")
        
        if confidence < confidence_threshold:
            logger.info(f"Skipping: ML confidence too low ({confidence:.4f} < {confidence_threshold:.2f})")
            self.stats["filtered_by_ml"] += 1
            return
        
        # Mark as viable opportunity
        self.stats["viable"] += 1
        
        # Execute the trade
        try:
            # Set pending transaction flag
            BALANCE_CACHE["pending_transaction"] = True
            
            # Execute trade
            self.execute_trade(trade_info, confidence)
            
            # Reset pending flag
            BALANCE_CACHE["pending_transaction"] = False
            
        except Exception as e:
            # Reset pending flag on error
            BALANCE_CACHE["pending_transaction"] = False
            logger.error(f"Error in process_trade_opportunity: {e}")
            logger.error(traceback.format_exc())
    
    def execute_trade(self, trade_info: Dict[str, Any], confidence: float):
        """Execute front-run and back-run trades"""
        # Start timing
        start_time = time.time()
        
        # Update attempt counter
        self.stats["attempted"] += 1
        
        try:
            # Get balance before trade using cached value to avoid RPC call
            sol_before, _ = self._get_cached_balances()
            
            # Extract configuration
            token_mint = config.get("TOKEN_MINT")
            front_run_amount = config.get("FRONT_RUN_AMOUNT", 0.35)
            slippage_bps = config.get("SLIPPAGE_BPS", 1000)
            
            # Execute front-run trade (buys token, then sells it)
            logger.info(f"Executing front-run trade: {front_run_amount} SOL to Token to SOL")
            
            # Define the async function - NOT calling it yet
            async def execute_front_run_async():
                return await self.trading_core.execute_front_run_trade(
                    token_mint=token_mint,
                    amount=front_run_amount,
                    slippage_bps=slippage_bps
                )
            
            # Execute using run_async_in_thread (passing the function, not calling it)
            result = run_async_in_thread(execute_front_run_async)
            
            if result.success:
                logger.info(f"Front-run trade executed successfully: {result.signature}")
                
                # Wait for transactions to settle
                time.sleep(0.5)
                
                # Force update of balance cache
                sol_after, _ = self._get_cached_balances(force_update=True)
                profit = sol_after - sol_before
                
                # Record execution time
                execution_time = (time.time() - start_time) * 1000  # in milliseconds
                self.stats["execution_times"].append(execution_time)
                
                # Update statistics
                self.stats["completed"] += 1
                self.stats["profit"] += profit
                self.last_trade_time = time.time()
                
                # Update ML signals with result
                self.update_ml_signals({
                    "last_profit": profit,
                    "execution_time": execution_time / 1000.0,
                    "trade_count": self.stats["completed"]
                })
                
                # Log detailed results
                logger.info(f"""
Trade #{self.stats["completed"]} Results:
Victim Size: {trade_info["amount"]:.4f} SOL
Transaction: {result.signature}
Profit: {profit:.6f} SOL
ML Confidence: {confidence:.4f}
Execution Time: {execution_time:.2f}ms
Total Profit: {self.stats["profit"]:.6f} SOL
Current Balance: {self.stats["current_balance"]:.6f} SOL
""")
                
                # Periodic stats update
                if self.stats["completed"] % 5 == 0:
                    self.print_stats()
                
                return True
            else:
                logger.error(f"Trade failed: {result.error}")
                return False
        
        except Exception as e:
            logger.error(f"Error executing trade: {e}")
            logger.error(traceback.format_exc())
            return False
    
    def websocket_thread(self):
        """Thread for WebSocket connection and message handling"""
        # Create asyncio loop for this thread
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        
        # Define the async function to run the WebSocket
        async def run_websocket():
            # Get WebSocket URL and API key
            ws_url = config.get('WS_URL')
            api_key = config.get('API_KEY')
            raydium_program_id = config.get("RAYDIUM_PROGRAM_ID")
            
            # Check if the URL already contains the API key
            if "api-key=" not in ws_url and "?" not in ws_url and api_key:
                # Use Bearer token authentication (more secure)
                headers = {
                    "Authorization": f"Bearer {api_key}"
                }
            else:
                # API key is already in URL or will be added below
                headers = {}
                
            # If URL doesn't have API key and we're not using headers, add it to URL
            if "api-key=" not in ws_url and not headers and api_key:
                ws_url = f"{ws_url}?api-key={api_key}"
                
            while self.is_running:
                try:
                    logger.info(f"Connecting to WebSocket: {ws_url.split('?')[0]}")  # Log URL without API key
                    
                    # Import websockets here to avoid potential import issues
                    import websockets
                    
                    # Connect with appropriate authentication
                    async with websockets.connect(
                        ws_url,
                        ping_interval=30,
                        ping_timeout=10,
                        close_timeout=10,
                        extra_headers=headers if headers else None
                    ) as ws:
                        # Subscribe to Raydium program logs
                        await ws.send(json.dumps({
                            "jsonrpc": "2.0",
                            "id": 1,
                            "method": "logsSubscribe",
                            "params": [
                                {"mentions": [raydium_program_id]},
                                {"commitment": "processed"}
                            ]
                        }))
                        
                        # Try to get public key for account subscription
                        try:
                            # Check if the method exists
                            if hasattr(self.trading_core, "get_public_key"):
                                public_key = self.trading_core.get_public_key()
                                
                                # Subscribe to our wallet account updates if we have the public key
                                if public_key:
                                    await ws.send(json.dumps({
                                        "jsonrpc": "2.0",
                                        "id": 2,
                                        "method": "accountSubscribe",
                                        "params": [
                                            public_key,
                                            {"encoding": "jsonParsed", "commitment": "processed"}
                                        ]
                                    }))
                                    logger.info(f"Subscribed to account updates for: {public_key}")
                        except Exception as e:
                            # If account subscription fails, just log it and continue
                            logger.warning(f"Failed to subscribe to account updates: {e}")
                        
                        logger.info("WebSocket connected, listening for Raydium transactions")
                        
                        # Process incoming messages
                        while self.is_running:
                            try:
                                # Set a timeout to periodically check is_running
                                message = await asyncio.wait_for(ws.recv(), timeout=1.0)
                                
                                # Parse response
                                data = json.loads(message)
                                
                                # Handle subscription confirmations
                                if "method" not in data and "id" in data:
                                    if data.get("id") == 1:
                                        # Raydium subscription confirmation
                                        subscription_id = data.get("result")
                                        logger.info(f"Raydium subscription confirmed: {subscription_id}")
                                        continue
                                    elif data.get("id") == 2:
                                        # Account subscription confirmation
                                        subscription_id = data.get("result")
                                        logger.info(f"Account subscription confirmed: {subscription_id}")
                                        continue
                                
                                # Process transaction logs
                                if "params" in data and "result" in data["params"] and "subscription" in data["params"]:
                                    result = data["params"]["result"]
                                    
                                    # Handle Raydium logs
                                    if "value" in result and "logs" in result["value"]:
                                        logs = result["value"]["logs"]
                                        
                                        for log in logs:
                                            if "ray_log:" in log:
                                                # Send log to trade processing thread via queue
                                                message_queue.put(("process_log", log))
                                    
                                    # Handle account update (balance change)
                                    elif "value" in result and result.get("value", {}).get("lamports") is not None:
                                        # Update our balance cache from WebSocket notification
                                        lamports = result["value"]["lamports"]
                                        sol_balance = lamports / 1_000_000_000  # Convert to SOL
                                        
                                        # Update cache if there's a significant change
                                        if abs(sol_balance - BALANCE_CACHE["sol"]) > 0.0001:
                                            BALANCE_CACHE["sol"] = sol_balance
                                            BALANCE_CACHE["last_update"] = time.time()
                                            
                                            logger.debug(f"Balance updated from WebSocket: {sol_balance:.6f} SOL")
                            
                            except asyncio.TimeoutError:
                                # This is expected due to our timeout, just continue
                                continue
                            except Exception as e:
                                logger.error(f"Error processing WebSocket message: {e}")
                        
                        # If we get here, is_running became False
                        logger.info("WebSocket loop terminated due to shutdown signal")
                        break
                
                except Exception as e:
                    logger.error(f"WebSocket connection error: {e}")
                    if self.is_running:
                        logger.info("Reconnecting in 5 seconds...")
                        await asyncio.sleep(5)
                    else:
                        break
            
            logger.info("WebSocket thread shutting down")
        
        # Run the async function in the loop
        try:
            loop.run_until_complete(run_websocket())
        except Exception as e:
            logger.error(f"Unhandled exception in WebSocket thread: {e}")
            logger.error(traceback.format_exc())
        finally:
            loop.close()
            logger.info("WebSocket thread loop closed")
    
    def balance_monitor_thread(self):
        """Thread for monitoring balances"""
        logger.info("Balance monitor thread started")
        
        while self.is_running:
            try:
                # Update and log balances (once per minute)
                self.update_and_log_balances()
                
                # Sleep until next update
                time.sleep(60)  # Update every minute
            
            except Exception as e:
                logger.error(f"Error monitoring balances: {e}")
                time.sleep(5)
        
        logger.info("Balance monitor thread shutting down")
    
    def system_health_thread(self):
        """Thread for monitoring system health"""
        logger.info("System health monitor thread started")
        
        while self.is_running:
            try:
                # Clear CUDA cache periodically
                if torch.cuda.is_available() and time.time() - self.last_memory_check > 300:  # Every 5 minutes
                    torch.cuda.empty_cache()
                    logger.debug("CUDA memory cache cleared")
                    self.last_memory_check = time.time()
                
                # Sleep for a while
                time.sleep(30)
            
            except Exception as e:
                logger.error(f"Error in system health check: {e}")
                time.sleep(5)
        
        logger.info("System health thread shutting down")
    
    def trade_processor_thread(self):
        """Thread for processing trades from queue"""
        logger.info("Trade processor thread started")
        
        while self.is_running:
            try:
                # Get message from queue with timeout
                try:
                    message_type, data = message_queue.get(timeout=1.0)
                except queue.Empty:
                    # This is expected, just continue
                    continue
                
                # Process message based on type
                if message_type == "process_log":
                    # Decode and process Raydium log
                    raw_data = self.decode_ray_log(data)
                    if raw_data:
                        trade_info = self.validate_trade(raw_data)
                        if trade_info:
                            self.process_trade_opportunity(trade_info)
                
                # Mark task as done
                message_queue.task_done()
            
            except Exception as e:
                logger.error(f"Error processing trade: {e}")
                time.sleep(1)
        
        logger.info("Trade processor thread shutting down")
    
    def print_stats(self):
        """Print current trading statistics"""
        runtime = time.time() - self.start_time
        runtime_hours = runtime / 3600
        
        # Calculate averages
        avg_execution_time = 0
        if self.stats["execution_times"]:
            avg_execution_time = sum(self.stats["execution_times"]) / len(self.stats["execution_times"])
        
        profit_per_hour = 0
        if runtime_hours > 0:
            profit_per_hour = self.stats["profit"] / runtime_hours
        
        success_rate = 0
        if self.stats["attempted"] > 0:
            success_rate = (self.stats["completed"] / self.stats["attempted"]) * 100
        
        # Calculate ROI
        initial_balance = self.stats["start_balance"]
        current_balance = self.stats["current_balance"]
        roi_percent = 0
        if initial_balance > 0:
            roi_percent = ((current_balance - initial_balance) / initial_balance) * 100
        
        # Simple text output
        logger.info(f"""
================= TRADING STATISTICS =================
Runtime: {runtime_hours:.2f} hours
Opportunities Found: {self.stats["opportunities"]}
Viable Opportunities: {self.stats["viable"]}
Attempted Trades: {self.stats["attempted"]}
Completed Trades: {self.stats["completed"]}
Success Rate: {success_rate:.1f}%
Avg Execution Time: {avg_execution_time:.2f}ms

FILTERING STATS:
Too Small: {self.stats["filtered_too_small"]}
Too Large: {self.stats["filtered_too_large"]}
ML Filtered: {self.stats["filtered_by_ml"]}
Time Filtered: {self.stats["filtered_by_time"]}

PROFIT METRICS:
Total Profit: {self.stats["profit"]:.6f} SOL
Profit per Hour: {profit_per_hour:.6f} SOL/hr
Starting Balance: {initial_balance:.6f} SOL
Current Balance: {current_balance:.6f} SOL
ROI: {roi_percent:.2f}%
======================================================
""")
    
    def handle_shutdown(self, signum, frame):
        """Handle graceful shutdown on signal"""
        logger.info("Shutdown signal received, stopping engine...")
        self.is_running = False
    
    def start(self):
        """Start the trading engine"""
        if self.is_running:
            logger.warning("Trading engine is already running")
            return
        
        self.is_running = True
        logger.info("Starting Trading Engine")
        
        # Print a welcome message
        print("""
╔══════════════════════════════════════════════════════════════╗
║                                                              ║
║  High-Performance Trading Bot                                ║
║  ───────────────────────────────                            ║
║                                                              ║
║  • FP16-optimized for RTX 4070 Ti                           ║
║  • GPU-accelerated ML for trade decisions                    ║
║  • Rust-powered Solana interactions                          ║
║  • Helius RPC for maximum performance                        ║
║                                                              ║
║  May your trades be profitable!                              ║
║                                                              ║
╚══════════════════════════════════════════════════════════════╝
""")
        
        # Start all threads
        self.threads = []
        
        # WebSocket thread for monitoring transactions
        websocket_thread = threading.Thread(target=self.websocket_thread, daemon=True)
        websocket_thread.start()
        self.threads.append(websocket_thread)
        
        # Balance monitor thread
        balance_thread = threading.Thread(target=self.balance_monitor_thread, daemon=True)
        balance_thread.start()
        self.threads.append(balance_thread)
        
        # System health monitor thread
        health_thread = threading.Thread(target=self.system_health_thread, daemon=True)
        health_thread.start()
        self.threads.append(health_thread)
        
        # Trade processor thread
        processor_thread = threading.Thread(target=self.trade_processor_thread, daemon=True)
        processor_thread.start()
        self.threads.append(processor_thread)
        
        logger.info(f"All threads started ({len(self.threads)} total)")
        
        # Wait for threads to complete (they will exit when is_running becomes False)
        try:
            # Keep main thread alive until interrupted
            while self.is_running:
                time.sleep(1)
        except KeyboardInterrupt:
            logger.info("Keyboard interrupt received, shutting down...")
            self.is_running = False
        finally:
            # Wait for threads to terminate
            for thread in self.threads:
                thread.join(timeout=2.0)
            
            # Stop the event loop
            global EVENT_LOOP
            if EVENT_LOOP and not EVENT_LOOP.is_closed():
                EVENT_LOOP.call_soon_threadsafe(EVENT_LOOP.stop)
                
            # Wait for event loop thread to terminate
            if self.event_loop_thread and self.event_loop_thread.is_alive():
                self.event_loop_thread.join(timeout=2.0)
            
            # Final stats
            self.print_stats()
            logger.info("Trading Engine stopped")


# Main entry point
if __name__ == "__main__":
    try:
        # Initialize the trading engine
        engine = TradingEngine()
        
        # Start the engine
        engine.start()
    except Exception as e:
        logger.error(f"Fatal error: {e}")
        logger.error(traceback.format_exc())
        sys.exit(1)