/// 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));
}
}
}
}
}