Backtesting code - deniyuda348/pump-fun-pump-swap-sniper-copy-bot GitHub Wiki

/// Monitor and sell based on trade info
    pub async fn monitor_and_sell(&self, trade_info: &TradeInfoFromToken) -> Result<bool> {
        self.logger.log(format!("Monitoring token: {}", trade_info.mint));
        
        // Update metrics with current price
        if let Some(price) = self.calculate_current_price(trade_info) {
            // Update metrics using entry API
            TOKEN_METRICS.entry(trade_info.mint.clone()).and_modify(|metrics| {
                metrics.current_price = price;
                
                // Update highest price if current price is higher
                if price > metrics.highest_price {
                    metrics.highest_price = price;
                }
                
                // Update time held
                if let Some(timestamp) = Some(trade_info.timestamp) {
                    if metrics.buy_timestamp > 0 {
                        metrics.time_held = timestamp.saturating_sub(metrics.buy_timestamp);
                    }
                }
            });
        }
        
        // Check all selling conditions
        let sell_reasons: Vec<String> = vec![
            self.check_liquidity_conditions(trade_info).await,
            self.check_volume_conditions(trade_info).await,
            self.check_price_conditions(trade_info).await,
            self.check_time_conditions(trade_info).await,
            self.check_wash_trading(trade_info),
            self.check_large_holder_actions(trade_info),
        ]
        .into_iter()
        .flatten()
        .collect();
        
        // If any conditions are met, execute sell
        if !sell_reasons.is_empty() {
            let reason = sell_reasons.join(", ");
            self.logger.log(format!("Sell conditions met: {}", reason).green().to_string());
            
            // Determine protocol based on token
            let protocol = match self.determine_best_protocol_for_token(&trade_info.mint).await {
                Ok(p) => p,
                Err(e) => {
                    self.logger.log(format!("Failed to determine protocol: {}", e).red().to_string());
                    return Err(anyhow!("Failed to determine protocol: {}", e));
                }
            };
            
            // Get enhanced TradeInfoFromToken for selling
            // We'll merge data from the original trade_info with additional data from metrics
            let enhanced_trade_info = match self.metrics_to_trade_info(&trade_info.mint, protocol.clone()).await {
                Ok(mut enhanced_info) => {
                    // Copy any available data from the original trade_info that might be useful
                    if enhanced_info.pool.is_none() && trade_info.pool.is_some() {
                        enhanced_info.pool = trade_info.pool.clone();
                    }
                    
                    if enhanced_info.pool_info.is_none() && trade_info.pool_info.is_some() {
                        enhanced_info.pool_info = trade_info.pool_info.clone();
                    }
                    
                    if enhanced_info.pool_base_token_reserves.is_none() && trade_info.pool_base_token_reserves.is_some() {
                        enhanced_info.pool_base_token_reserves = trade_info.pool_base_token_reserves;
                    }
                    
                    if enhanced_info.pool_quote_token_reserves.is_none() && trade_info.pool_quote_token_reserves.is_some() {
                        enhanced_info.pool_quote_token_reserves = trade_info.pool_quote_token_reserves;
                    }
                    
                    if enhanced_info.coin_creator.is_none() && trade_info.coin_creator.is_some() {
                        enhanced_info.coin_creator = trade_info.coin_creator.clone();
                    }
                    
                    enhanced_info
                },
                Err(e) => {
                    self.logger.log(format!("Failed to create enhanced trade info: {}", e).red().to_string());
                    return Err(anyhow!("Failed to create enhanced trade info: {}", e));
                }
            };
            
            // Execute progressive sell with the enhanced trade_info
            match self.progressive_sell(&trade_info.mint, &enhanced_trade_info, protocol).await {
                Ok(_) => {
                    self.logger.log(format!("Successfully sold token: {}", trade_info.mint).green().to_string());
                    
                    // Record the trade
                    if let Err(e) = self.record_trade_execution(
                        &trade_info.mint, 
                        &reason, 
                        0.0, // Amount will be filled in by execute_sell
                        &format!("{:?}", trade_info.dex_type)
                    ).await {
                        self.logger.log(format!("Failed to record trade: {}", e).red().to_string());
                    }
                    
                    return Ok(true);
                },
                Err(e) => {
                    self.logger.log(format!("Failed to sell token: {} - {}", trade_info.mint, e).red().to_string());
                    return Err(anyhow!("Failed to sell token: {}", e));
                }
            }
        }
        
        Ok(false)
    }
 
   /// Run a backtest of the selling strategy on historical data
    pub async fn backtest_strategy(
        &self,
        historical_data: &[TradeInfoFromToken],
        selling_config: Option<SellingConfig>,
    ) -> Vec<TradeExecutionRecord> {
        let logger = Logger::new("[SELLING-STRATEGY-BACKTEST] => ".cyan().to_string());
        logger.log("Starting backtest of selling strategy...".to_string());
        
        // Create a new engine with the provided config or default
        let config = selling_config.unwrap_or_else(|| self.config.clone());
        let backtest_engine = SellingEngine {
            app_state: self.app_state.clone(),
            swap_config: self.swap_config.clone(),
            config,
            logger: Logger::new("[BACKTEST] => ".cyan().to_string()),
            token_manager: TokenManager::new(),
            is_progressive_sell: self.is_progressive_sell
        };
        
        // Clear global state for testing
        TOKEN_METRICS.clear();
        TOKEN_TRACKING.clear();
        HISTORICAL_TRADES.entry(()).or_insert_with(|| VecDeque::with_capacity(100)).clear();
        
        // Process all historical trades
        let mut tokens_bought: HashSet<String> = HashSet::new();
        
        for trade in historical_data {
            logger.log(format!("Processing trade: {} at timestamp {}", trade.mint, trade.timestamp));
            
            if trade.is_buy {
                // This is a buy - record it
                if !tokens_bought.contains(&trade.mint) {
                    // Calculate cost and amount
                    let token_amount = match trade.token_amount {
                        Some(amount) => amount as f64,
                        None => trade.token_amount_f64,
                    };
                    
                    let sol_amount = match trade.sol_amount {
                        Some(amount) => (amount as f64) / 1_000_000_000.0, // Convert from lamports
                        None => 0.0,
                    };
                    
                    if token_amount > 0.0 && sol_amount > 0.0 {
                        logger.log(format!("Recording buy: {} tokens for {} SOL", token_amount, sol_amount));
                        if let Err(e) = backtest_engine.record_buy(&trade.mint, token_amount, sol_amount).await {
                            logger.log(format!("Error recording buy: {}", e).red().to_string());
                            continue; // Skip this trade if recording fails
                        }
                        tokens_bought.insert(trade.mint.clone());
                    }
                }
            } else {
                // This is a sell - evaluate selling condition
                if tokens_bought.contains(&trade.mint) {
                    match backtest_engine.monitor_and_sell(trade).await {
                        Ok(sold) => {
                            if sold {
                                tokens_bought.remove(&trade.mint);
                            }
                        },
                        Err(e) => {
                            logger.log(format!("Error in backtest: {}", e).red().to_string());
                        }
                    }
                }
            }
        }
        
        // Get results from HISTORICAL_TRADES
        let results: Vec<TradeExecutionRecord> = HISTORICAL_TRADES
            .get(&())
            .map(|history| history.clone().into_iter().collect())
            .unwrap_or_default();
        
        // Log summary
        let mut total_profit = 0.0;
        let mut win_count = 0;
        let mut loss_count = 0;
        
        for record in &results {
            total_profit += record.pnl;
        
            // Count winning vs losing trades
            if record.pnl > 0.0 {
                win_count += 1;
            } else {
                loss_count += 1;
            }
        }
        
        logger.log(format!(
            "Backtest complete: {} trades, {} wins, {} losses, {:.2}% total return",
            results.len(),
            win_count,
            loss_count,
            total_profit * 100.0
        ));
        
        results
    }

    /// Generate backtest report from trade records
    pub fn generate_backtest_report(&self, records: &[TradeExecutionRecord]) -> String {
        let logger = Logger::new("[SELLING-STRATEGY-REPORT] => ".cyan().to_string());
        
        if records.is_empty() {
            return "No trades recorded in backtest.".to_string();
        }
        
        // Calculate statistics
        let mut total_pnl = 0.0;
        let mut winning_trades = 0;
        let mut losing_trades = 0;
        let mut avg_win = 0.0;
        let mut avg_loss = 0.0;
        let mut max_win = 0.0;
        let mut max_loss = 0.0;
        let mut hold_times = Vec::new();
        let mut reason_counts = HashMap::new();
        
        let mut prev_timestamp = records[0].timestamp;
        
        for record in records {
            total_pnl += record.pnl;
            
            // Count winning vs losing trades
            if record.pnl > 0.0 {
                winning_trades += 1;
                avg_win += record.pnl;
                if record.pnl > max_win {
                    max_win = record.pnl;
                }
            } else {
                losing_trades += 1;
                avg_loss += record.pnl;
                if record.pnl < max_loss {
                    max_loss = record.pnl;
                }
            }
            
            // Calculate hold time
            if record.timestamp > prev_timestamp {
                hold_times.push(record.timestamp - prev_timestamp);
            }
            prev_timestamp = record.timestamp;
            
            // Count reasons
            let reason = record.reason.clone();
            *reason_counts.entry(reason).or_insert(0) += 1;
        }
        
        // Calculate averages
        avg_win = if winning_trades > 0 { avg_win / winning_trades as f64 } else { 0.0 };
        avg_loss = if losing_trades > 0 { avg_loss / losing_trades as f64 } else { 0.0 };
        
        // Calculate average hold time
        let avg_hold_time = if !hold_times.is_empty() {
            hold_times.iter().sum::<u64>() as f64 / hold_times.len() as f64
        } else {
            0.0
        };
        
        // Build report
        let mut report = String::new();
        report.push_str(&format!("Backtest Report\n"));
        report.push_str(&format!("==============\n\n"));
        report.push_str(&format!("Total Trades: {}\n", records.len()));
        report.push_str(&format!("Winning Trades: {} ({:.1}%)\n", 
            winning_trades, (winning_trades as f64 / records.len() as f64) * 100.0));
        report.push_str(&format!("Losing Trades: {} ({:.1}%)\n", 
            losing_trades, (losing_trades as f64 / records.len() as f64) * 100.0));
        report.push_str(&format!("Total PnL: {:.2}%\n", total_pnl * 100.0));
        report.push_str(&format!("Average Win: {:.2}%\n", avg_win * 100.0));
        report.push_str(&format!("Average Loss: {:.2}%\n", avg_loss * 100.0));
        report.push_str(&format!("Max Win: {:.2}%\n", max_win * 100.0));
        report.push_str(&format!("Max Loss: {:.2}%\n", max_loss * 100.0));
        report.push_str(&format!("Average Hold Time: {:.2} seconds\n", avg_hold_time));
        
        // Report on exit reasons
        report.push_str(&format!("\nExit Reasons:\n"));
        for (reason, count) in reason_counts.iter() {
            report.push_str(&format!("  {}: {} ({:.1}%)\n", 
                reason, 
                count, 
                (*count as f64 / records.len() as f64) * 100.0
            ));
        }
        
        // Log statistics
        logger.log(format!("Generated backtest report: {} trades, {:.2}% total return", 
            records.len(), total_pnl * 100.0));
        
        report
    }

   
   /// Determine best protocol for selling a token
    async fn determine_best_protocol_for_token(&self, token_mint: &str) -> Result<SwapProtocol> {
        // Try PumpSwap first
        let pump_swap = PumpSwap::new(
            self.app_state.wallet.clone(),
            Some(self.app_state.rpc_client.clone()),
            Some(self.app_state.rpc_nonblocking_client.clone()),
        );
        
        match pump_swap.get_token_price(token_mint).await {
            Ok(_) => {
                self.logger.log(format!("Found token on PumpSwap: {}", token_mint).green().to_string());
                return Ok(SwapProtocol::PumpSwap);
            },
            Err(_) => {
                // Try PumpFun next
                let pump_fun = Pump::new(
                    self.app_state.rpc_nonblocking_client.clone(),
                    self.app_state.rpc_client.clone(),
                    self.app_state.wallet.clone(),
                );
                
                match pump_fun.get_token_price(token_mint).await {
                    Ok(_) => {
                        self.logger.log(format!("Found token on PumpFun: {}", token_mint).green().to_string());
                        return Ok(SwapProtocol::PumpFun);
                    },
                    Err(e) => {
                        return Err(anyhow!("Token not found on any supported DEX: {}", e));
                    }
                }
            }
        }
    }
⚠️ **GitHub.com Fallback** ⚠️