├── src ├── lib.rs ├── error.rs ├── main.rs ├── config.rs ├── wallet.rs ├── pumpportal.rs ├── monitoring.rs ├── strategies.rs ├── bot.rs └── trading.rs ├── .gitignore ├── Cargo.toml ├── config.example.toml └── README.md /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod bot; 2 | pub mod config; 3 | pub mod error; 4 | pub mod pumpportal; 5 | pub mod trading; 6 | pub mod wallet; 7 | pub mod monitoring; 8 | pub mod strategies; 9 | 10 | pub use error::BotError; 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Rust 2 | target/ 3 | Cargo.lock 4 | 5 | # IDE 6 | .vscode/ 7 | .idea/ 8 | *.swp 9 | *.swo 10 | 11 | # OS 12 | .DS_Store 13 | Thumbs.db 14 | 15 | # Bot specific 16 | wallet.json 17 | config.toml 18 | *.log 19 | metrics.json 20 | config_backup.toml 21 | 22 | # Secrets 23 | .env 24 | *.key 25 | *.pem 26 | 27 | # Build artifacts 28 | dist/ 29 | build/ 30 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub enum BotError { 5 | #[error("Configuration error: {0}")] 6 | Config(String), 7 | 8 | #[error("Solana client error: {0}")] 9 | SolanaClient(#[from] solana_client::client_error::ClientError), 10 | 11 | #[error("HTTP request error: {0}")] 12 | Http(#[from] reqwest::Error), 13 | 14 | #[error("Serialization error: {0}")] 15 | Serialization(#[from] serde_json::Error), 16 | 17 | #[error("Wallet error: {0}")] 18 | Wallet(String), 19 | 20 | #[error("Trading error: {0}")] 21 | Trading(String), 22 | 23 | #[error("PumpPortal API error: {0}")] 24 | PumpPortal(String), 25 | 26 | #[error("Insufficient funds: {0}")] 27 | InsufficientFunds(String), 28 | 29 | #[error("Transaction failed: {0}")] 30 | TransactionFailed(String), 31 | 32 | #[error("Invalid token address: {0}")] 33 | InvalidTokenAddress(String), 34 | 35 | #[error("Rate limit exceeded")] 36 | RateLimitExceeded, 37 | 38 | #[error("Network error: {0}")] 39 | Network(String), 40 | 41 | #[error("Unknown error: {0}")] 42 | Unknown(String), 43 | } 44 | 45 | pub type Result = std::result::Result; 46 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "solana-pumpfun-bot" 3 | version = "0.1.0" 4 | edition = "2021" 5 | authors = ["Your Name "] 6 | description = "A Solana PumpFun trading bot with PumpPortal integration" 7 | license = "MIT" 8 | 9 | [dependencies] 10 | # Solana dependencies 11 | solana-client = "1.18" 12 | solana-sdk = "1.18" 13 | spl-token = "4.0" 14 | anchor-client = "0.30" 15 | anchor-lang = "0.30" 16 | 17 | # HTTP and async 18 | reqwest = { version = "0.11", features = ["json"] } 19 | tokio = { version = "1.0", features = ["full"] } 20 | futures = "0.3" 21 | 22 | # Serialization 23 | serde = { version = "1.0", features = ["derive"] } 24 | serde_json = "1.0" 25 | 26 | # Error handling 27 | anyhow = "1.0" 28 | thiserror = "1.0" 29 | 30 | # Logging 31 | log = "0.4" 32 | env_logger = "0.10" 33 | 34 | # Time and utilities 35 | chrono = { version = "0.4", features = ["serde"] } 36 | uuid = { version = "1.0", features = ["v4"] } 37 | 38 | # Configuration 39 | config = "0.14" 40 | toml = "0.8" 41 | clap = { version = "4.0", features = ["derive"] } 42 | 43 | # Math and calculations 44 | rust_decimal = "1.32" 45 | rust_decimal_macros = "1.32" 46 | 47 | # WebSocket support for real-time data 48 | tokio-tungstenite = "0.20" 49 | tungstenite = "0.20" 50 | 51 | # Base58 encoding 52 | bs58 = "0.5" 53 | 54 | # Keypair management 55 | solana-keygen = "1.18" 56 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use clap::Parser; 3 | use log::{error, info, warn}; 4 | use solana_pumpfun_bot::{ 5 | bot::TradingBot, 6 | config::Config, 7 | error::BotError, 8 | }; 9 | use std::process; 10 | 11 | #[derive(Parser)] 12 | #[command(name = "solana-pumpfun-bot")] 13 | #[command(about = "A Solana PumpFun trading bot with PumpPortal integration")] 14 | struct Cli { 15 | /// Configuration file path 16 | #[arg(short, long, default_value = "config.toml")] 17 | config: String, 18 | 19 | /// Enable debug logging 20 | #[arg(short, long)] 21 | debug: bool, 22 | 23 | /// Dry run mode (no actual trades) 24 | #[arg(long)] 25 | dry_run: bool, 26 | } 27 | 28 | #[tokio::main] 29 | async fn main() { 30 | let cli = Cli::parse(); 31 | 32 | // Initialize logging 33 | let log_level = if cli.debug { "debug" } else { "info" }; 34 | env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(log_level)) 35 | .init(); 36 | 37 | info!("Starting Solana PumpFun Trading Bot"); 38 | 39 | // Load configuration 40 | let config = match Config::load(&cli.config) { 41 | Ok(config) => { 42 | info!("Configuration loaded successfully"); 43 | config 44 | } 45 | Err(e) => { 46 | error!("Failed to load configuration: {}", e); 47 | process::exit(1); 48 | } 49 | }; 50 | 51 | // Create and run the trading bot 52 | let mut bot = match TradingBot::new(config, cli.dry_run).await { 53 | Ok(bot) => { 54 | info!("Trading bot initialized successfully"); 55 | bot 56 | } 57 | Err(e) => { 58 | error!("Failed to initialize trading bot: {}", e); 59 | process::exit(1); 60 | } 61 | }; 62 | 63 | // Run the bot 64 | if let Err(e) = bot.run().await { 65 | error!("Bot error: {}", e); 66 | process::exit(1); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /config.example.toml: -------------------------------------------------------------------------------- 1 | # Example Configuration for Solana PumpFun Trading Bot 2 | # Copy this file to config.toml and modify as needed 3 | 4 | # Solana RPC Configuration 5 | # Use a reliable RPC endpoint for better performance 6 | rpc_url = "https://api.mainnet-beta.solana.com" 7 | # Alternative RPC endpoints: 8 | # rpc_url = "https://solana-api.projectserum.com" 9 | # rpc_url = "https://rpc.ankr.com/solana" 10 | 11 | # Wallet Configuration 12 | # Path to your Solana wallet keypair file 13 | wallet_path = "wallet.json" 14 | # Generate a new wallet with: solana-keygen new --outfile wallet.json 15 | 16 | # Trading Configuration 17 | [trading] 18 | # Maximum slippage tolerance (percentage) 19 | max_slippage = 5.0 20 | 21 | # Minimum liquidity required (in lamports, 1 SOL = 1,000,000,000 lamports) 22 | min_liquidity = 100_000_000 # 100 SOL 23 | 24 | # Maximum amount to spend on a single buy (in lamports) 25 | max_buy_amount = 1_000_000_000 # 1 SOL 26 | 27 | # Maximum amount to sell in a single transaction (in lamports) 28 | max_sell_amount = 1_000_000_000 # 1 SOL 29 | 30 | # Take profit percentage 31 | profit_target_percent = 20.0 32 | 33 | # Stop loss percentage 34 | stop_loss_percent = 10.0 35 | 36 | # Cooldown between trades on the same token (seconds) 37 | cooldown_seconds = 60 38 | 39 | # Maximum number of concurrent positions 40 | max_positions = 5 41 | 42 | # PumpPortal API Configuration 43 | [pumpportal] 44 | # PumpPortal API endpoint 45 | api_url = "https://api.pumpportal.fun" 46 | 47 | # Optional API key for premium features 48 | api_key = "" 49 | 50 | # How often to check for new tokens (milliseconds) 51 | refresh_interval_ms = 5000 52 | 53 | # Minimum market cap filter (in USD) 54 | min_market_cap = 1_000_000 # $1M 55 | 56 | # Maximum market cap filter (in USD) 57 | max_market_cap = 10_000_000 # $10M 58 | 59 | # Minimum number of holders required 60 | min_holders = 100 61 | 62 | # Maximum token age (hours) 63 | max_age_hours = 24 64 | 65 | # Monitoring Configuration 66 | [monitoring] 67 | # Log level: trace, debug, info, warn, error 68 | log_level = "info" 69 | 70 | # Save trade history to file 71 | save_trades = true 72 | 73 | # Optional webhook URL for alerts (Discord, Slack, etc.) 74 | webhook_url = "" 75 | 76 | # Alert thresholds 77 | [monitoring.alert_thresholds] 78 | # Alert if maximum drawdown exceeds this percentage 79 | max_drawdown_percent = 20.0 80 | 81 | # Alert if daily profit is below this percentage 82 | min_daily_profit_percent = 5.0 83 | 84 | # Alert if daily loss exceeds this percentage 85 | max_daily_loss_percent = 15.0 86 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Result; 2 | use serde::{Deserialize, Serialize}; 3 | use std::path::Path; 4 | 5 | #[derive(Debug, Clone, Serialize, Deserialize)] 6 | pub struct Config { 7 | pub rpc_url: String, 8 | pub wallet_path: String, 9 | pub trading: TradingConfig, 10 | pub pumpportal: PumpPortalConfig, 11 | pub monitoring: MonitoringConfig, 12 | } 13 | 14 | #[derive(Debug, Clone, Serialize, Deserialize)] 15 | pub struct TradingConfig { 16 | pub max_slippage: f64, 17 | pub min_liquidity: u64, 18 | pub max_buy_amount: u64, 19 | pub max_sell_amount: u64, 20 | pub profit_target_percent: f64, 21 | pub stop_loss_percent: f64, 22 | pub cooldown_seconds: u64, 23 | pub max_positions: usize, 24 | } 25 | 26 | #[derive(Debug, Clone, Serialize, Deserialize)] 27 | pub struct PumpPortalConfig { 28 | pub api_url: String, 29 | pub api_key: Option, 30 | pub refresh_interval_ms: u64, 31 | pub min_market_cap: u64, 32 | pub max_market_cap: u64, 33 | pub min_holders: u32, 34 | pub max_age_hours: u32, 35 | } 36 | 37 | #[derive(Debug, Clone, Serialize, Deserialize)] 38 | pub struct MonitoringConfig { 39 | pub log_level: String, 40 | pub save_trades: bool, 41 | pub webhook_url: Option, 42 | pub alert_thresholds: AlertThresholds, 43 | } 44 | 45 | #[derive(Debug, Clone, Serialize, Deserialize)] 46 | pub struct AlertThresholds { 47 | pub max_drawdown_percent: f64, 48 | pub min_daily_profit_percent: f64, 49 | pub max_daily_loss_percent: f64, 50 | } 51 | 52 | impl Default for Config { 53 | fn default() -> Self { 54 | Self { 55 | rpc_url: "https://api.mainnet-beta.solana.com".to_string(), 56 | wallet_path: "wallet.json".to_string(), 57 | trading: TradingConfig::default(), 58 | pumpportal: PumpPortalConfig::default(), 59 | monitoring: MonitoringConfig::default(), 60 | } 61 | } 62 | } 63 | 64 | impl Default for TradingConfig { 65 | fn default() -> Self { 66 | Self { 67 | max_slippage: 5.0, 68 | min_liquidity: 100_000_000, // 100 SOL 69 | max_buy_amount: 1_000_000_000, // 1 SOL 70 | max_sell_amount: 1_000_000_000, // 1 SOL 71 | profit_target_percent: 20.0, 72 | stop_loss_percent: 10.0, 73 | cooldown_seconds: 60, 74 | max_positions: 5, 75 | } 76 | } 77 | } 78 | 79 | impl Default for PumpPortalConfig { 80 | fn default() -> Self { 81 | Self { 82 | api_url: "https://api.pumpportal.fun".to_string(), 83 | api_key: None, 84 | refresh_interval_ms: 5000, 85 | min_market_cap: 1_000_000, // $1M 86 | max_market_cap: 10_000_000, // $10M 87 | min_holders: 100, 88 | max_age_hours: 24, 89 | } 90 | } 91 | } 92 | 93 | impl Default for MonitoringConfig { 94 | fn default() -> Self { 95 | Self { 96 | log_level: "info".to_string(), 97 | save_trades: true, 98 | webhook_url: None, 99 | alert_thresholds: AlertThresholds::default(), 100 | } 101 | } 102 | } 103 | 104 | impl Default for AlertThresholds { 105 | fn default() -> Self { 106 | Self { 107 | max_drawdown_percent: 20.0, 108 | min_daily_profit_percent: 5.0, 109 | max_daily_loss_percent: 15.0, 110 | } 111 | } 112 | } 113 | 114 | impl Config { 115 | pub fn load>(path: P) -> Result { 116 | let config_str = std::fs::read_to_string(path) 117 | .map_err(|e| crate::error::BotError::Config(format!("Failed to read config file: {}", e)))?; 118 | let config: Config = toml::from_str(&config_str) 119 | .map_err(|e| crate::error::BotError::Config(format!("Failed to parse config: {}", e)))?; 120 | Ok(config) 121 | } 122 | 123 | pub fn save>(&self, path: P) -> Result<()> { 124 | let config_str = toml::to_string_pretty(self) 125 | .map_err(|e| crate::error::BotError::Config(format!("Failed to serialize config: {}", e)))?; 126 | std::fs::write(path, config_str) 127 | .map_err(|e| crate::error::BotError::Config(format!("Failed to write config file: {}", e)))?; 128 | Ok(()) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/wallet.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{BotError, Result}; 2 | use solana_client::rpc_client::RpcClient; 3 | use solana_sdk::{ 4 | commitment_config::CommitmentConfig, 5 | pubkey::Pubkey, 6 | signature::{Keypair, Signature}, 7 | signer::Signer, 8 | transaction::Transaction, 9 | }; 10 | use spl_token::instruction as token_instruction; 11 | use std::str::FromStr; 12 | 13 | #[derive(Clone)] 14 | pub struct WalletManager { 15 | keypair: Keypair, 16 | rpc_client: RpcClient, 17 | wallet_address: Pubkey, 18 | } 19 | 20 | impl WalletManager { 21 | pub fn new(keypair: Keypair, rpc_url: String) -> Self { 22 | let rpc_client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed()); 23 | let wallet_address = keypair.pubkey(); 24 | 25 | Self { 26 | keypair, 27 | rpc_client, 28 | wallet_address, 29 | } 30 | } 31 | 32 | pub fn from_file(path: &str) -> Result { 33 | let keypair = Keypair::read_from_file(path) 34 | .map_err(|e| BotError::Wallet(format!("Failed to read wallet file: {}", e)))?; 35 | 36 | // Default to mainnet RPC 37 | let rpc_url = "https://api.mainnet-beta.solana.com".to_string(); 38 | Ok(Self::new(keypair, rpc_url)) 39 | } 40 | 41 | pub fn generate_new() -> Result { 42 | let keypair = Keypair::new(); 43 | let rpc_url = "https://api.mainnet-beta.solana.com".to_string(); 44 | Ok(Self::new(keypair, rpc_url)) 45 | } 46 | 47 | pub fn get_address(&self) -> Pubkey { 48 | self.wallet_address 49 | } 50 | 51 | pub async fn get_balance(&self) -> Result { 52 | self.rpc_client 53 | .get_balance(&self.wallet_address) 54 | .await 55 | .map_err(|e| BotError::SolanaClient(e)) 56 | } 57 | 58 | pub async fn get_token_balance(&self, token_mint: &Pubkey) -> Result { 59 | let token_accounts = self.rpc_client 60 | .get_token_accounts_by_owner( 61 | &self.wallet_address, 62 | solana_client::rpc_request::TokenAccountsFilter::Mint(*token_mint), 63 | ) 64 | .await 65 | .map_err(|e| BotError::SolanaClient(e))?; 66 | 67 | if token_accounts.is_empty() { 68 | return Ok(0); 69 | } 70 | 71 | let account_info = self.rpc_client 72 | .get_token_account_balance(&token_accounts[0].pubkey) 73 | .await 74 | .map_err(|e| BotError::SolanaClient(e))?; 75 | 76 | Ok(account_info.amount.parse::() 77 | .map_err(|e| BotError::Wallet(format!("Failed to parse token balance: {}", e)))?) 78 | } 79 | 80 | pub async fn create_token_account(&self, token_mint: &Pubkey) -> Result { 81 | let token_account = Keypair::new(); 82 | let rent = self.rpc_client 83 | .get_minimum_balance_for_rent_exemption(spl_token::state::Account::LEN) 84 | .await 85 | .map_err(|e| BotError::SolanaClient(e))?; 86 | 87 | let create_account_ix = solana_sdk::system_instruction::create_account( 88 | &self.wallet_address, 89 | &token_account.pubkey(), 90 | rent, 91 | spl_token::state::Account::LEN as u64, 92 | &spl_token::id(), 93 | ); 94 | 95 | let initialize_account_ix = token_instruction::initialize_account( 96 | &spl_token::id(), 97 | &token_account.pubkey(), 98 | token_mint, 99 | &self.wallet_address, 100 | )?; 101 | 102 | let recent_blockhash = self.rpc_client 103 | .get_latest_blockhash() 104 | .await 105 | .map_err(|e| BotError::SolanaClient(e))?; 106 | 107 | let transaction = Transaction::new_signed_with_payer( 108 | &[create_account_ix, initialize_account_ix], 109 | Some(&self.wallet_address), 110 | &[&self.keypair, &token_account], 111 | recent_blockhash, 112 | ); 113 | 114 | self.rpc_client 115 | .send_and_confirm_transaction(&transaction) 116 | .await 117 | .map_err(|e| BotError::TransactionFailed(format!("Failed to create token account: {}", e)))?; 118 | 119 | Ok(token_account.pubkey()) 120 | } 121 | 122 | pub async fn sign_and_send_transaction(&self, transaction: Transaction) -> Result { 123 | let signature = self.rpc_client 124 | .send_and_confirm_transaction(&transaction) 125 | .await 126 | .map_err(|e| BotError::TransactionFailed(format!("Transaction failed: {}", e)))?; 127 | 128 | Ok(signature) 129 | } 130 | 131 | pub fn get_keypair(&self) -> &Keypair { 132 | &self.keypair 133 | } 134 | 135 | pub fn get_rpc_client(&self) -> &RpcClient { 136 | &self.rpc_client 137 | } 138 | 139 | pub async fn get_recent_blockhash(&self) -> Result { 140 | self.rpc_client 141 | .get_latest_blockhash() 142 | .await 143 | .map_err(|e| BotError::SolanaClient(e)) 144 | } 145 | 146 | pub async fn estimate_transaction_fee(&self, transaction: &Transaction) -> Result { 147 | self.rpc_client 148 | .get_fee_for_message(&transaction.message) 149 | .await 150 | .map_err(|e| BotError::SolanaClient(e)) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/pumpportal.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{BotError, Result}; 2 | use reqwest::Client; 3 | use serde::{Deserialize, Serialize}; 4 | use std::collections::HashMap; 5 | use tokio::time::{sleep, Duration}; 6 | 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | pub struct TokenInfo { 9 | pub address: String, 10 | pub symbol: String, 11 | pub name: String, 12 | pub decimals: u8, 13 | pub market_cap: u64, 14 | pub holders: u32, 15 | pub age_hours: u32, 16 | pub liquidity: u64, 17 | pub price_usd: f64, 18 | pub price_change_24h: f64, 19 | pub volume_24h: u64, 20 | pub created_at: String, 21 | } 22 | 23 | #[derive(Debug, Clone, Serialize, Deserialize)] 24 | pub struct PumpPortalResponse { 25 | pub success: bool, 26 | pub data: Vec, 27 | pub message: Option, 28 | } 29 | 30 | #[derive(Debug, Clone, Serialize, Deserialize)] 31 | pub struct TokenMetrics { 32 | pub address: String, 33 | pub price: f64, 34 | pub volume_5m: u64, 35 | pub volume_1h: u64, 36 | pub volume_24h: u64, 37 | pub holders: u32, 38 | pub market_cap: u64, 39 | pub liquidity: u64, 40 | pub price_change_5m: f64, 41 | pub price_change_1h: f64, 42 | pub price_change_24h: f64, 43 | } 44 | 45 | pub struct PumpPortalClient { 46 | client: Client, 47 | api_url: String, 48 | api_key: Option, 49 | refresh_interval: Duration, 50 | } 51 | 52 | impl PumpPortalClient { 53 | pub fn new(api_url: String, api_key: Option, refresh_interval_ms: u64) -> Self { 54 | Self { 55 | client: Client::new(), 56 | api_url, 57 | api_key, 58 | refresh_interval: Duration::from_millis(refresh_interval_ms), 59 | } 60 | } 61 | 62 | pub async fn get_new_tokens(&self) -> Result> { 63 | let url = format!("{}/api/tokens/new", self.api_url); 64 | let mut request = self.client.get(&url); 65 | 66 | if let Some(key) = &self.api_key { 67 | request = request.header("Authorization", format!("Bearer {}", key)); 68 | } 69 | 70 | let response = request 71 | .send() 72 | .await 73 | .map_err(|e| BotError::Http(e))?; 74 | 75 | if !response.status().is_success() { 76 | return Err(BotError::PumpPortal(format!( 77 | "API request failed with status: {}", 78 | response.status() 79 | ))); 80 | } 81 | 82 | let pump_response: PumpPortalResponse = response 83 | .json() 84 | .await 85 | .map_err(|e| BotError::Serialization(e))?; 86 | 87 | if !pump_response.success { 88 | return Err(BotError::PumpPortal( 89 | pump_response.message.unwrap_or("Unknown API error".to_string()) 90 | )); 91 | } 92 | 93 | Ok(pump_response.data) 94 | } 95 | 96 | pub async fn get_token_metrics(&self, token_address: &str) -> Result { 97 | let url = format!("{}/api/tokens/{}/metrics", self.api_url, token_address); 98 | let mut request = self.client.get(&url); 99 | 100 | if let Some(key) = &self.api_key { 101 | request = request.header("Authorization", format!("Bearer {}", key)); 102 | } 103 | 104 | let response = request 105 | .send() 106 | .await 107 | .map_err(|e| BotError::Http(e))?; 108 | 109 | if !response.status().is_success() { 110 | return Err(BotError::PumpPortal(format!( 111 | "Failed to get metrics for token {}: {}", 112 | token_address, 113 | response.status() 114 | ))); 115 | } 116 | 117 | let metrics: TokenMetrics = response 118 | .json() 119 | .await 120 | .map_err(|e| BotError::Serialization(e))?; 121 | 122 | Ok(metrics) 123 | } 124 | 125 | pub async fn get_trending_tokens(&self) -> Result> { 126 | let url = format!("{}/api/tokens/trending", self.api_url); 127 | let mut request = self.client.get(&url); 128 | 129 | if let Some(key) = &self.api_key { 130 | request = request.header("Authorization", format!("Bearer {}", key)); 131 | } 132 | 133 | let response = request 134 | .send() 135 | .await 136 | .map_err(|e| BotError::Http(e))?; 137 | 138 | if !response.status().is_success() { 139 | return Err(BotError::PumpPortal(format!( 140 | "Failed to get trending tokens: {}", 141 | response.status() 142 | ))); 143 | } 144 | 145 | let pump_response: PumpPortalResponse = response 146 | .json() 147 | .await 148 | .map_err(|e| BotError::Serialization(e))?; 149 | 150 | if !pump_response.success { 151 | return Err(BotError::PumpPortal( 152 | pump_response.message.unwrap_or("Unknown API error".to_string()) 153 | )); 154 | } 155 | 156 | Ok(pump_response.data) 157 | } 158 | 159 | pub async fn filter_tokens_by_criteria( 160 | &self, 161 | tokens: Vec, 162 | min_market_cap: u64, 163 | max_market_cap: u64, 164 | min_holders: u32, 165 | max_age_hours: u32, 166 | ) -> Vec { 167 | tokens 168 | .into_iter() 169 | .filter(|token| { 170 | token.market_cap >= min_market_cap 171 | && token.market_cap <= max_market_cap 172 | && token.holders >= min_holders 173 | && token.age_hours <= max_age_hours 174 | }) 175 | .collect() 176 | } 177 | 178 | pub async fn start_monitoring(&self, mut callback: F) -> Result<()> 179 | where 180 | F: FnMut(Vec) -> Result<()> + Send + 'static, 181 | { 182 | loop { 183 | match self.get_new_tokens().await { 184 | Ok(tokens) => { 185 | if let Err(e) = callback(tokens).await { 186 | log::error!("Error in monitoring callback: {}", e); 187 | } 188 | } 189 | Err(e) => { 190 | log::error!("Failed to fetch new tokens: {}", e); 191 | } 192 | } 193 | 194 | sleep(self.refresh_interval).await; 195 | } 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/monitoring.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Result; 2 | use crate::trading::{Position, Trade}; 3 | use chrono::{DateTime, Utc}; 4 | use serde::{Deserialize, Serialize}; 5 | use std::collections::HashMap; 6 | 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | pub struct BotMetrics { 9 | pub total_trades: usize, 10 | pub winning_trades: usize, 11 | pub losing_trades: usize, 12 | pub total_profit_loss: f64, 13 | pub win_rate: f64, 14 | pub average_profit: f64, 15 | pub average_loss: f64, 16 | pub max_drawdown: f64, 17 | pub current_positions: usize, 18 | pub total_volume_traded: u64, 19 | pub uptime_seconds: u64, 20 | pub last_updated: DateTime, 21 | } 22 | 23 | #[derive(Debug, Clone, Serialize, Deserialize)] 24 | pub struct Alert { 25 | pub id: String, 26 | pub level: AlertLevel, 27 | pub message: String, 28 | pub timestamp: DateTime, 29 | pub acknowledged: bool, 30 | } 31 | 32 | #[derive(Debug, Clone, Serialize, Deserialize)] 33 | pub enum AlertLevel { 34 | Info, 35 | Warning, 36 | Error, 37 | Critical, 38 | } 39 | 40 | pub struct MonitoringSystem { 41 | metrics: BotMetrics, 42 | alerts: Vec, 43 | start_time: DateTime, 44 | webhook_url: Option, 45 | alert_thresholds: AlertThresholds, 46 | } 47 | 48 | #[derive(Debug, Clone, Serialize, Deserialize)] 49 | pub struct AlertThresholds { 50 | pub max_drawdown_percent: f64, 51 | pub min_daily_profit_percent: f64, 52 | pub max_daily_loss_percent: f64, 53 | } 54 | 55 | impl MonitoringSystem { 56 | pub fn new(webhook_url: Option, alert_thresholds: AlertThresholds) -> Self { 57 | Self { 58 | metrics: BotMetrics::default(), 59 | alerts: Vec::new(), 60 | start_time: Utc::now(), 61 | webhook_url, 62 | alert_thresholds, 63 | } 64 | } 65 | 66 | pub fn update_metrics(&mut self, trades: &[Trade], positions: &HashMap) { 67 | self.metrics.total_trades = trades.len(); 68 | self.metrics.current_positions = positions.len(); 69 | self.metrics.last_updated = Utc::now(); 70 | self.metrics.uptime_seconds = (Utc::now() - self.start_time).num_seconds() as u64; 71 | 72 | // Calculate profit/loss metrics 73 | let mut total_profit_loss = 0.0; 74 | let mut winning_trades = 0; 75 | let mut losing_trades = 0; 76 | let mut total_volume = 0u64; 77 | 78 | for trade in trades { 79 | total_volume += trade.amount; 80 | 81 | if let Some(pl) = trade.profit_loss { 82 | total_profit_loss += pl; 83 | if pl > 0.0 { 84 | winning_trades += 1; 85 | } else if pl < 0.0 { 86 | losing_trades += 1; 87 | } 88 | } 89 | } 90 | 91 | self.metrics.total_profit_loss = total_profit_loss; 92 | self.metrics.winning_trades = winning_trades; 93 | self.metrics.losing_trades = losing_trades; 94 | self.metrics.total_volume_traded = total_volume; 95 | 96 | // Calculate rates 97 | if self.metrics.total_trades > 0 { 98 | self.metrics.win_rate = (winning_trades as f64 / self.metrics.total_trades as f64) * 100.0; 99 | } 100 | 101 | if winning_trades > 0 { 102 | let total_wins: f64 = trades.iter() 103 | .filter_map(|t| t.profit_loss) 104 | .filter(|&pl| pl > 0.0) 105 | .sum(); 106 | self.metrics.average_profit = total_wins / winning_trades as f64; 107 | } 108 | 109 | if losing_trades > 0 { 110 | let total_losses: f64 = trades.iter() 111 | .filter_map(|t| t.profit_loss) 112 | .filter(|&pl| pl < 0.0) 113 | .sum(); 114 | self.metrics.average_loss = total_losses.abs() / losing_trades as f64; 115 | } 116 | 117 | // Calculate max drawdown 118 | self.metrics.max_drawdown = self.calculate_max_drawdown(trades); 119 | 120 | // Check for alerts 121 | self.check_alerts(); 122 | } 123 | 124 | fn calculate_max_drawdown(&self, trades: &[Trade]) -> f64 { 125 | let mut peak = 0.0; 126 | let mut max_drawdown = 0.0; 127 | let mut running_pl = 0.0; 128 | 129 | for trade in trades { 130 | if let Some(pl) = trade.profit_loss { 131 | running_pl += pl; 132 | if running_pl > peak { 133 | peak = running_pl; 134 | } 135 | let drawdown = peak - running_pl; 136 | if drawdown > max_drawdown { 137 | max_drawdown = drawdown; 138 | } 139 | } 140 | } 141 | 142 | max_drawdown 143 | } 144 | 145 | fn check_alerts(&mut self) { 146 | // Check drawdown alert 147 | if self.metrics.max_drawdown > self.alert_thresholds.max_drawdown_percent { 148 | self.add_alert( 149 | AlertLevel::Warning, 150 | format!( 151 | "Max drawdown exceeded: {:.2}% (threshold: {:.2}%)", 152 | self.metrics.max_drawdown, 153 | self.alert_thresholds.max_drawdown_percent 154 | ), 155 | ); 156 | } 157 | 158 | // Check daily profit/loss 159 | let daily_pl_percent = self.calculate_daily_pl_percent(); 160 | if daily_pl_percent < -self.alert_thresholds.max_daily_loss_percent { 161 | self.add_alert( 162 | AlertLevel::Error, 163 | format!( 164 | "Daily loss exceeded: {:.2}% (threshold: {:.2}%)", 165 | daily_pl_percent.abs(), 166 | self.alert_thresholds.max_daily_loss_percent 167 | ), 168 | ); 169 | } 170 | 171 | // Check win rate 172 | if self.metrics.total_trades > 10 && self.metrics.win_rate < 30.0 { 173 | self.add_alert( 174 | AlertLevel::Warning, 175 | format!("Low win rate: {:.2}%", self.metrics.win_rate), 176 | ); 177 | } 178 | } 179 | 180 | fn calculate_daily_pl_percent(&self) -> f64 { 181 | // This is a simplified calculation 182 | // In practice, you'd calculate based on daily P&L 183 | self.metrics.total_profit_loss 184 | } 185 | 186 | fn add_alert(&mut self, level: AlertLevel, message: String) { 187 | let alert = Alert { 188 | id: uuid::Uuid::new_v4().to_string(), 189 | level, 190 | message, 191 | timestamp: Utc::now(), 192 | acknowledged: false, 193 | }; 194 | 195 | self.alerts.push(alert); 196 | 197 | // Send webhook if configured 198 | if let Some(ref webhook_url) = self.webhook_url { 199 | self.send_webhook_alert(&alert); 200 | } 201 | } 202 | 203 | async fn send_webhook_alert(&self, alert: &Alert) { 204 | if let Some(ref webhook_url) = self.webhook_url { 205 | let payload = serde_json::json!({ 206 | "text": format!("[{}] {}", alert.level, alert.message), 207 | "timestamp": alert.timestamp.to_rfc3339(), 208 | "bot_metrics": self.metrics 209 | }); 210 | 211 | if let Err(e) = reqwest::Client::new() 212 | .post(webhook_url) 213 | .json(&payload) 214 | .send() 215 | .await 216 | { 217 | log::error!("Failed to send webhook alert: {}", e); 218 | } 219 | } 220 | } 221 | 222 | pub fn get_metrics(&self) -> &BotMetrics { 223 | &self.metrics 224 | } 225 | 226 | pub fn get_alerts(&self) -> &[Alert] { 227 | &self.alerts 228 | } 229 | 230 | pub fn acknowledge_alert(&mut self, alert_id: &str) -> bool { 231 | if let Some(alert) = self.alerts.iter_mut().find(|a| a.id == alert_id) { 232 | alert.acknowledged = true; 233 | true 234 | } else { 235 | false 236 | } 237 | } 238 | 239 | pub fn get_unacknowledged_alerts(&self) -> Vec<&Alert> { 240 | self.alerts.iter().filter(|a| !a.acknowledged).collect() 241 | } 242 | 243 | pub fn save_metrics_to_file(&self, path: &str) -> Result<()> { 244 | let json = serde_json::to_string_pretty(&self.metrics) 245 | .map_err(|e| crate::error::BotError::Serialization(e))?; 246 | std::fs::write(path, json) 247 | .map_err(|e| crate::error::BotError::Unknown(format!("Failed to write metrics file: {}", e)))?; 248 | Ok(()) 249 | } 250 | 251 | pub fn load_metrics_from_file(&mut self, path: &str) -> Result<()> { 252 | if std::path::Path::new(path).exists() { 253 | let json = std::fs::read_to_string(path) 254 | .map_err(|e| crate::error::BotError::Unknown(format!("Failed to read metrics file: {}", e)))?; 255 | self.metrics = serde_json::from_str(&json) 256 | .map_err(|e| crate::error::BotError::Serialization(e))?; 257 | } 258 | Ok(()) 259 | } 260 | } 261 | 262 | impl Default for BotMetrics { 263 | fn default() -> Self { 264 | Self { 265 | total_trades: 0, 266 | winning_trades: 0, 267 | losing_trades: 0, 268 | total_profit_loss: 0.0, 269 | win_rate: 0.0, 270 | average_profit: 0.0, 271 | average_loss: 0.0, 272 | max_drawdown: 0.0, 273 | current_positions: 0, 274 | total_volume_traded: 0, 275 | uptime_seconds: 0, 276 | last_updated: Utc::now(), 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solana PumpFun Trading Bot 🚀 2 | 3 | A sophisticated Rust-based trading bot for Solana's PumpFun platform with PumpPortal integration. This bot automatically identifies and trades new tokens based on multiple trading strategies with comprehensive monitoring and risk management. 4 | 5 | ## 🎯 Features 6 | 7 | - **Multi-Strategy Trading**: Implements momentum, mean reversion, breakout, volume spike, and holder growth strategies 8 | - **PumpPortal Integration**: Real-time token discovery and analysis 9 | - **Risk Management**: Configurable stop-loss, take-profit, and position sizing 10 | - **Real-time Monitoring**: Comprehensive metrics, alerts, and webhook notifications 11 | - **Dry Run Mode**: Test strategies without real money 12 | - **Wallet Management**: Secure keypair handling and transaction management 13 | - **Configurable**: Extensive configuration options for all parameters 14 | 15 | ## 🏗️ Architecture 16 | 17 | ### System Components 18 | 19 | ``` 20 | ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ 21 | │ PumpPortal │ │ Trading Bot │ │ Solana RPC │ 22 | │ API │◄──►│ │◄──►│ │ 23 | └─────────────────┘ └─────────────────┘ └─────────────────┘ 24 | │ 25 | ▼ 26 | ┌─────────────────┐ 27 | │ Wallet │ 28 | │ Management │ 29 | └─────────────────┘ 30 | ``` 31 | 32 | ### Bot Flow Diagram 33 | 34 | ``` 35 | ┌─────────────────┐ 36 | │ Start Bot │ 37 | └─────────┬───────┘ 38 | │ 39 | ▼ 40 | ┌─────────────────┐ 41 | │ Load Config │ 42 | └─────────┬───────┘ 43 | │ 44 | ▼ 45 | ┌─────────────────┐ 46 | │ Initialize │ 47 | │ Components │ 48 | └─────────┬───────┘ 49 | │ 50 | ▼ 51 | ┌─────────────────┐ 52 | │ Main Loop │ 53 | └─────────┬───────┘ 54 | │ 55 | ▼ 56 | ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ 57 | │ Fetch New │ │ Analyze Tokens │ │ Check Exit │ 58 | │ Tokens │───►│ with Strategies │───►│ Conditions │ 59 | └─────────────────┘ └─────────┬───────┘ └─────────┬───────┘ 60 | │ │ 61 | ▼ ▼ 62 | ┌─────────────────┐ ┌─────────────────┐ 63 | │ Execute Trades │ │ Update Metrics │ 64 | └─────────┬───────┘ └─────────┬───────┘ 65 | │ │ 66 | └──────────┬───────────┘ 67 | │ 68 | ▼ 69 | ┌─────────────────┐ 70 | │ Sleep & Repeat │ 71 | └─────────────────┘ 72 | ``` 73 | 74 | ## 🧠 Trading Logic 75 | 76 | ### Strategy Engine 77 | 78 | The bot implements five distinct trading strategies: 79 | 80 | #### 1. Momentum Strategy 81 | - **Trigger**: Price increase ≥ 5% with volume ratio ≥ 2x 82 | - **Logic**: Identifies tokens with strong upward price momentum 83 | - **Confidence**: Based on price change percentage 84 | 85 | #### 2. Mean Reversion Strategy 86 | - **Trigger**: Price drop ≥ 10% with good liquidity ratio 87 | - **Logic**: Assumes temporary price drops will recover 88 | - **Confidence**: Based on magnitude of price drop 89 | 90 | #### 3. Breakout Strategy 91 | - **Trigger**: Volume spike ≥ 3x with price momentum ≥ 2% 92 | - **Logic**: Catches tokens breaking out of consolidation 93 | - **Confidence**: Based on volume spike magnitude 94 | 95 | #### 4. Volume Spike Strategy 96 | - **Trigger**: Volume multiplier ≥ 5x with ≥ 50 holders 97 | - **Logic**: Identifies sudden interest in tokens 98 | - **Confidence**: Based on volume spike and holder count 99 | 100 | #### 5. Holder Growth Strategy 101 | - **Trigger**: ≥ 100 holders with ≥ $500K market cap 102 | - **Logic**: Organic growth in token adoption 103 | - **Confidence**: Based on holder-to-market-cap ratio 104 | 105 | ### Risk Management 106 | 107 | ``` 108 | ┌─────────────────┐ 109 | │ Position Entry │ 110 | └─────────┬───────┘ 111 | │ 112 | ▼ 113 | ┌─────────────────┐ 114 | │ Check Criteria │ 115 | │ - Market Cap │ 116 | │ - Holders │ 117 | │ - Age │ 118 | │ - Liquidity │ 119 | └─────────┬───────┘ 120 | │ 121 | ▼ 122 | ┌─────────────────┐ 123 | │ Calculate Size │ 124 | │ - Confidence │ 125 | │ - Max Amount │ 126 | │ - Available SOL │ 127 | └─────────┬───────┘ 128 | │ 129 | ▼ 130 | ┌─────────────────┐ 131 | │ Execute Trade │ 132 | └─────────┬───────┘ 133 | │ 134 | ▼ 135 | ┌─────────────────┐ 136 | │ Monitor Position│ 137 | │ - Profit Target │ 138 | │ - Stop Loss │ 139 | │ - Time Decay │ 140 | └─────────────────┘ 141 | ``` 142 | 143 | ## 📊 Monitoring & Alerts 144 | 145 | ### Metrics Tracked 146 | - Total trades executed 147 | - Win/loss ratio 148 | - Total profit/loss 149 | - Maximum drawdown 150 | - Current positions 151 | - Volume traded 152 | - Uptime 153 | 154 | ### Alert System 155 | - **Critical**: System failures, wallet issues 156 | - **Error**: Daily loss thresholds exceeded 157 | - **Warning**: High drawdown, low win rate 158 | - **Info**: General status updates 159 | 160 | ### Webhook Integration 161 | Supports Discord, Slack, and custom webhook endpoints for real-time notifications. 162 | 163 | ## 🚀 Quick Start 164 | 165 | ### Prerequisites 166 | - Rust 1.70+ installed 167 | - Solana CLI tools 168 | - A Solana wallet with some SOL for trading 169 | 170 | ### Installation 171 | 172 | 1. **Clone the repository** 173 | ```bash 174 | git clone 175 | cd solana-pumpfun-bot 176 | ``` 177 | 178 | 2. **Install dependencies** 179 | ```bash 180 | cargo build --release 181 | ``` 182 | 183 | 3. **Generate or import wallet** 184 | ```bash 185 | # Generate new wallet 186 | solana-keygen new --outfile wallet.json 187 | 188 | # Or import existing wallet 189 | solana-keygen recover 'prompt://?key=0/0' --outfile wallet.json 190 | ``` 191 | 192 | 4. **Configure the bot** 193 | ```bash 194 | cp config.example.toml config.toml 195 | # Edit config.toml with your settings 196 | ``` 197 | 198 | 5. **Run the bot** 199 | ```bash 200 | # Dry run (recommended first) 201 | cargo run -- --dry-run 202 | 203 | # Live trading 204 | cargo run 205 | ``` 206 | 207 | ### Configuration 208 | 209 | Key configuration parameters: 210 | 211 | ```toml 212 | [trading] 213 | max_buy_amount = 1_000_000_000 # 1 SOL max per trade 214 | profit_target_percent = 20.0 # Take profit at 20% 215 | stop_loss_percent = 10.0 # Stop loss at 10% 216 | max_positions = 5 # Max concurrent positions 217 | 218 | [pumpportal] 219 | min_market_cap = 1_000_000 # $1M minimum 220 | max_market_cap = 10_000_000 # $10M maximum 221 | min_holders = 100 # Minimum holders 222 | ``` 223 | 224 | ## 🔧 Advanced Usage 225 | 226 | ### Custom Strategies 227 | 228 | Add custom trading strategies by implementing the `StrategyConfig`: 229 | 230 | ```rust 231 | let custom_strategy = StrategyConfig { 232 | strategy: TradingStrategy::Custom, 233 | parameters: { 234 | let mut params = HashMap::new(); 235 | params.insert("custom_param".to_string(), 1.5); 236 | params 237 | }, 238 | enabled: true, 239 | }; 240 | ``` 241 | 242 | ### Webhook Alerts 243 | 244 | Configure Discord webhook for alerts: 245 | 246 | ```toml 247 | [monitoring] 248 | webhook_url = "https://discord.com/api/webhooks/YOUR_WEBHOOK_URL" 249 | ``` 250 | 251 | ### API Integration 252 | 253 | The bot integrates with PumpPortal API for: 254 | - Real-time token discovery 255 | - Market data and metrics 256 | - Holder information 257 | - Price and volume data 258 | 259 | ## 📈 Performance Optimization 260 | 261 | ### RPC Endpoints 262 | Use reliable RPC endpoints for better performance: 263 | - Mainnet: `https://api.mainnet-beta.solana.com` 264 | - Helius: `https://mainnet.helius-rpc.com/?api-key=YOUR_KEY` 265 | - QuickNode: `https://YOUR_ENDPOINT.solana-mainnet.quiknode.pro/YOUR_KEY/` 266 | 267 | ### Strategy Tuning 268 | - Adjust confidence thresholds based on market conditions 269 | - Modify cooldown periods to prevent overtrading 270 | - Fine-tune profit/loss targets for your risk tolerance 271 | 272 | ## 🛡️ Security Considerations 273 | 274 | - **Wallet Security**: Never share your private key 275 | - **API Keys**: Use environment variables for sensitive data 276 | - **Dry Run**: Always test with dry run mode first 277 | - **Position Limits**: Set appropriate position size limits 278 | - **Monitoring**: Regularly check bot performance and alerts 279 | 280 | ## 📝 Logging 281 | 282 | The bot provides comprehensive logging: 283 | - Trade execution details 284 | - Strategy analysis results 285 | - Error messages and warnings 286 | - Performance metrics 287 | - Alert notifications 288 | 289 | Log levels: `trace`, `debug`, `info`, `warn`, `error` 290 | 291 | ## 🤝 Contributing 292 | 293 | 1. Fork the repository 294 | 2. Create a feature branch 295 | 3. Make your changes 296 | 4. Add tests if applicable 297 | 5. Submit a pull request 298 | 299 | ## 📄 License 300 | 301 | This project is licensed under the MIT License - see the LICENSE file for details. 302 | 303 | ## ⚠️ Disclaimer 304 | 305 | This software is for educational purposes only. Trading cryptocurrencies involves substantial risk of loss. The authors are not responsible for any financial losses. Always do your own research and never invest more than you can afford to lose. 306 | 307 | ## 🆘 Support 308 | 309 | For support and questions: 310 | - Create an issue on GitHub 311 | - Check the documentation 312 | - Review the configuration examples 313 | - **Telegram Contact**: [@Kat_logic](https://t.me/Kat_logic) 314 | 315 | ## 🔄 Updates 316 | 317 | Stay updated with the latest features and improvements: 318 | - Watch the repository for releases 319 | - Check the changelog 320 | - Follow best practices for updates 321 | 322 | --- 323 | 324 | **Happy Trading! 🚀📈** 325 | -------------------------------------------------------------------------------- /src/strategies.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Result; 2 | use crate::pumpportal::TokenInfo; 3 | use rust_decimal::Decimal; 4 | use serde::{Deserialize, Serialize}; 5 | use std::collections::HashMap; 6 | 7 | #[derive(Debug, Clone, Serialize, Deserialize)] 8 | pub enum TradingStrategy { 9 | Momentum, 10 | MeanReversion, 11 | Breakout, 12 | VolumeSpike, 13 | HolderGrowth, 14 | } 15 | 16 | #[derive(Debug, Clone, Serialize, Deserialize)] 17 | pub struct StrategyConfig { 18 | pub strategy: TradingStrategy, 19 | pub parameters: HashMap, 20 | pub enabled: bool, 21 | } 22 | 23 | #[derive(Debug, Clone)] 24 | pub struct TradingSignal { 25 | pub token: TokenInfo, 26 | pub action: Action, 27 | pub confidence: f64, 28 | pub reason: String, 29 | pub expected_price: Option, 30 | } 31 | 32 | #[derive(Debug, Clone, PartialEq)] 33 | pub enum Action { 34 | Buy, 35 | Sell, 36 | Hold, 37 | } 38 | 39 | pub struct StrategyEngine { 40 | strategies: Vec, 41 | position_history: HashMap>, 42 | } 43 | 44 | impl StrategyEngine { 45 | pub fn new(strategies: Vec) -> Self { 46 | Self { 47 | strategies, 48 | position_history: HashMap::new(), 49 | } 50 | } 51 | 52 | pub fn analyze_token(&mut self, token: &TokenInfo) -> Result> { 53 | let mut signals = Vec::new(); 54 | 55 | for strategy_config in &self.strategies { 56 | if !strategy_config.enabled { 57 | continue; 58 | } 59 | 60 | match strategy_config.strategy { 61 | TradingStrategy::Momentum => { 62 | if let Some(signal) = self.momentum_strategy(token, &strategy_config.parameters)? { 63 | signals.push(signal); 64 | } 65 | } 66 | TradingStrategy::MeanReversion => { 67 | if let Some(signal) = self.mean_reversion_strategy(token, &strategy_config.parameters)? { 68 | signals.push(signal); 69 | } 70 | } 71 | TradingStrategy::Breakout => { 72 | if let Some(signal) = self.breakout_strategy(token, &strategy_config.parameters)? { 73 | signals.push(signal); 74 | } 75 | } 76 | TradingStrategy::VolumeSpike => { 77 | if let Some(signal) = self.volume_spike_strategy(token, &strategy_config.parameters)? { 78 | signals.push(signal); 79 | } 80 | } 81 | TradingStrategy::HolderGrowth => { 82 | if let Some(signal) = self.holder_growth_strategy(token, &strategy_config.parameters)? { 83 | signals.push(signal); 84 | } 85 | } 86 | } 87 | } 88 | 89 | Ok(signals) 90 | } 91 | 92 | fn momentum_strategy(&self, token: &TokenInfo, params: &HashMap) -> Result> { 93 | let min_price_change = params.get("min_price_change").unwrap_or(&5.0); 94 | let min_volume_ratio = params.get("min_volume_ratio").unwrap_or(&2.0); 95 | 96 | // Check if price is increasing significantly 97 | if token.price_change_24h >= *min_price_change { 98 | // Check if volume is also increasing 99 | let volume_ratio = token.volume_24h as f64 / token.liquidity as f64; 100 | if volume_ratio >= *min_volume_ratio { 101 | return Ok(Some(TradingSignal { 102 | token: token.clone(), 103 | action: Action::Buy, 104 | confidence: (token.price_change_24h / 100.0).min(1.0), 105 | reason: format!( 106 | "Momentum: Price up {:.2}%, Volume ratio {:.2}", 107 | token.price_change_24h, 108 | volume_ratio 109 | ), 110 | expected_price: Some(token.price_usd * 1.1), 111 | })); 112 | } 113 | } 114 | 115 | Ok(None) 116 | } 117 | 118 | fn mean_reversion_strategy(&self, token: &TokenInfo, params: &HashMap) -> Result> { 119 | let max_price_change = params.get("max_price_change").unwrap_or(&-10.0); 120 | let min_liquidity_ratio = params.get("min_liquidity_ratio").unwrap_or(&0.1); 121 | 122 | // Check if price has dropped significantly but liquidity is still good 123 | if token.price_change_24h <= *max_price_change { 124 | let liquidity_ratio = token.liquidity as f64 / token.market_cap as f64; 125 | if liquidity_ratio >= *min_liquidity_ratio { 126 | return Ok(Some(TradingSignal { 127 | token: token.clone(), 128 | action: Action::Buy, 129 | confidence: (-token.price_change_24h / 100.0).min(1.0), 130 | reason: format!( 131 | "Mean Reversion: Price down {:.2}%, Liquidity ratio {:.2}", 132 | token.price_change_24h, 133 | liquidity_ratio 134 | ), 135 | expected_price: Some(token.price_usd * 1.05), 136 | })); 137 | } 138 | } 139 | 140 | Ok(None) 141 | } 142 | 143 | fn breakout_strategy(&self, token: &TokenInfo, params: &HashMap) -> Result> { 144 | let min_volume_spike = params.get("min_volume_spike").unwrap_or(&3.0); 145 | let min_price_momentum = params.get("min_price_momentum").unwrap_or(&2.0); 146 | 147 | // Check for volume spike with price momentum 148 | let volume_spike = token.volume_24h as f64 / token.liquidity as f64; 149 | if volume_spike >= *min_volume_spike && token.price_change_24h >= *min_price_momentum { 150 | return Ok(Some(TradingSignal { 151 | token: token.clone(), 152 | action: Action::Buy, 153 | confidence: (volume_spike / 10.0).min(1.0), 154 | reason: format!( 155 | "Breakout: Volume spike {:.2}x, Price momentum {:.2}%", 156 | volume_spike, 157 | token.price_change_24h 158 | ), 159 | expected_price: Some(token.price_usd * 1.15), 160 | })); 161 | } 162 | 163 | Ok(None) 164 | } 165 | 166 | fn volume_spike_strategy(&self, token: &TokenInfo, params: &HashMap) -> Result> { 167 | let min_volume_multiplier = params.get("min_volume_multiplier").unwrap_or(&5.0); 168 | let min_holders = params.get("min_holders").unwrap_or(&50.0) as u32; 169 | 170 | // Check for sudden volume increase with decent holder count 171 | let volume_multiplier = token.volume_24h as f64 / token.liquidity as f64; 172 | if volume_multiplier >= *min_volume_multiplier && token.holders >= min_holders { 173 | return Ok(Some(TradingSignal { 174 | token: token.clone(), 175 | action: Action::Buy, 176 | confidence: (volume_multiplier / 20.0).min(1.0), 177 | reason: format!( 178 | "Volume Spike: {:.2}x volume, {} holders", 179 | volume_multiplier, 180 | token.holders 181 | ), 182 | expected_price: Some(token.price_usd * 1.2), 183 | })); 184 | } 185 | 186 | Ok(None) 187 | } 188 | 189 | fn holder_growth_strategy(&self, token: &TokenInfo, params: &HashMap) -> Result> { 190 | let min_holders = params.get("min_holders").unwrap_or(&100.0) as u32; 191 | let min_market_cap = params.get("min_market_cap").unwrap_or(&500_000.0) as u64; 192 | 193 | // Look for tokens with growing holder base and decent market cap 194 | if token.holders >= min_holders && token.market_cap >= min_market_cap { 195 | let holder_ratio = token.holders as f64 / (token.market_cap as f64 / 1_000_000.0); 196 | if holder_ratio >= 0.1 { // At least 0.1 holders per $1M market cap 197 | return Ok(Some(TradingSignal { 198 | token: token.clone(), 199 | action: Action::Buy, 200 | confidence: (holder_ratio / 2.0).min(1.0), 201 | reason: format!( 202 | "Holder Growth: {} holders, ${:.0}M market cap", 203 | token.holders, 204 | token.market_cap as f64 / 1_000_000.0 205 | ), 206 | expected_price: Some(token.price_usd * 1.08), 207 | })); 208 | } 209 | } 210 | 211 | Ok(None) 212 | } 213 | 214 | pub fn update_position_history(&mut self, token_address: String, price: f64) { 215 | self.position_history 216 | .entry(token_address) 217 | .or_insert_with(Vec::new) 218 | .push(price); 219 | } 220 | 221 | pub fn get_position_history(&self, token_address: &str) -> Option<&Vec> { 222 | self.position_history.get(token_address) 223 | } 224 | 225 | pub fn should_sell(&self, token_address: &str, current_price: f64, entry_price: f64) -> bool { 226 | let profit_percent = ((current_price - entry_price) / entry_price) * 100.0; 227 | let loss_percent = ((entry_price - current_price) / entry_price) * 100.0; 228 | 229 | // Take profit at 20% or stop loss at 10% 230 | profit_percent >= 20.0 || loss_percent >= 10.0 231 | } 232 | } 233 | 234 | impl Default for StrategyConfig { 235 | fn default() -> Self { 236 | let mut parameters = HashMap::new(); 237 | parameters.insert("min_price_change".to_string(), 5.0); 238 | parameters.insert("min_volume_ratio".to_string(), 2.0); 239 | parameters.insert("max_price_change".to_string(), -10.0); 240 | parameters.insert("min_liquidity_ratio".to_string(), 0.1); 241 | parameters.insert("min_volume_spike".to_string(), 3.0); 242 | parameters.insert("min_price_momentum".to_string(), 2.0); 243 | parameters.insert("min_volume_multiplier".to_string(), 5.0); 244 | parameters.insert("min_holders".to_string(), 100.0); 245 | parameters.insert("min_market_cap".to_string(), 500_000.0); 246 | 247 | Self { 248 | strategy: TradingStrategy::Momentum, 249 | parameters, 250 | enabled: true, 251 | } 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /src/bot.rs: -------------------------------------------------------------------------------- 1 | use crate::config::Config; 2 | use crate::error::{BotError, Result}; 3 | use crate::monitoring::{AlertThresholds, MonitoringSystem}; 4 | use crate::pumpportal::{PumpPortalClient, TokenInfo}; 5 | use crate::strategies::{StrategyConfig, StrategyEngine, TradingStrategy}; 6 | use crate::trading::{TradingEngine, Trade}; 7 | use crate::wallet::WalletManager; 8 | use log::{error, info, warn}; 9 | use std::collections::HashMap; 10 | use tokio::time::{sleep, Duration}; 11 | 12 | pub struct TradingBot { 13 | config: Config, 14 | wallet: WalletManager, 15 | pumpportal: PumpPortalClient, 16 | strategy_engine: StrategyEngine, 17 | trading_engine: TradingEngine, 18 | monitoring: MonitoringSystem, 19 | dry_run: bool, 20 | running: bool, 21 | } 22 | 23 | impl TradingBot { 24 | pub async fn new(config: Config, dry_run: bool) -> Result { 25 | // Initialize wallet 26 | let wallet = if std::path::Path::new(&config.wallet_path).exists() { 27 | WalletManager::from_file(&config.wallet_path)? 28 | } else { 29 | warn!("Wallet file not found, generating new wallet"); 30 | let wallet = WalletManager::generate_new()?; 31 | // Save the new wallet 32 | wallet.get_keypair().write_to_file(&config.wallet_path) 33 | .map_err(|e| BotError::Wallet(format!("Failed to save wallet: {}", e)))?; 34 | wallet 35 | }; 36 | 37 | info!("Wallet address: {}", wallet.get_address()); 38 | 39 | // Initialize PumpPortal client 40 | let pumpportal = PumpPortalClient::new( 41 | config.pumpportal.api_url.clone(), 42 | config.pumpportal.api_key.clone(), 43 | config.pumpportal.refresh_interval_ms, 44 | ); 45 | 46 | // Initialize strategy engine 47 | let strategies = vec![ 48 | StrategyConfig { 49 | strategy: TradingStrategy::Momentum, 50 | parameters: HashMap::new(), 51 | enabled: true, 52 | }, 53 | StrategyConfig { 54 | strategy: TradingStrategy::VolumeSpike, 55 | parameters: HashMap::new(), 56 | enabled: true, 57 | }, 58 | StrategyConfig { 59 | strategy: TradingStrategy::HolderGrowth, 60 | parameters: HashMap::new(), 61 | enabled: true, 62 | }, 63 | ]; 64 | let strategy_engine = StrategyEngine::new(strategies); 65 | 66 | // Initialize trading engine 67 | let trading_engine = TradingEngine::new( 68 | &wallet, 69 | config.trading.max_positions, 70 | config.trading.max_buy_amount, 71 | config.trading.max_sell_amount, 72 | config.trading.profit_target_percent, 73 | config.trading.stop_loss_percent, 74 | config.trading.cooldown_seconds, 75 | ); 76 | 77 | // Initialize monitoring system 78 | let monitoring = MonitoringSystem::new( 79 | config.monitoring.webhook_url.clone(), 80 | config.monitoring.alert_thresholds.clone(), 81 | ); 82 | 83 | Ok(Self { 84 | config, 85 | wallet, 86 | pumpportal, 87 | strategy_engine, 88 | trading_engine, 89 | monitoring, 90 | dry_run, 91 | running: false, 92 | } 93 | } 94 | 95 | pub async fn run(&mut self) -> Result<()> { 96 | self.running = true; 97 | info!("Starting trading bot (dry_run: {})", self.dry_run); 98 | 99 | // Start monitoring loop 100 | let mut last_check = std::time::Instant::now(); 101 | 102 | while self.running { 103 | // Check for new tokens 104 | if let Err(e) = self.check_new_tokens().await { 105 | error!("Error checking new tokens: {}", e); 106 | } 107 | 108 | // Check exit conditions for existing positions 109 | if let Err(e) = self.check_exit_conditions().await { 110 | error!("Error checking exit conditions: {}", e); 111 | } 112 | 113 | // Update monitoring metrics 114 | self.update_monitoring().await; 115 | 116 | // Sleep for refresh interval 117 | sleep(Duration::from_millis(self.config.pumpportal.refresh_interval_ms)).await; 118 | 119 | // Periodic status update 120 | if last_check.elapsed().as_secs() >= 60 { 121 | self.log_status().await; 122 | last_check = std::time::Instant::now(); 123 | } 124 | } 125 | 126 | info!("Trading bot stopped"); 127 | Ok(()) 128 | } 129 | 130 | async fn check_new_tokens(&mut self) -> Result<()> { 131 | // Get new tokens from PumpPortal 132 | let tokens = self.pumpportal.get_new_tokens().await?; 133 | 134 | // Filter tokens based on criteria 135 | let filtered_tokens = self.pumpportal.filter_tokens_by_criteria( 136 | tokens, 137 | self.config.pumpportal.min_market_cap, 138 | self.config.pumpportal.max_market_cap, 139 | self.config.pumpportal.min_holders, 140 | self.config.pumpportal.max_age_hours, 141 | ).await; 142 | 143 | info!("Found {} new tokens matching criteria", filtered_tokens.len()); 144 | 145 | // Analyze each token 146 | for token in filtered_tokens { 147 | if let Err(e) = self.analyze_and_trade_token(token).await { 148 | error!("Error analyzing token: {}", e); 149 | } 150 | } 151 | 152 | Ok(()) 153 | } 154 | 155 | async fn analyze_and_trade_token(&mut self, token: TokenInfo) -> Result<()> { 156 | info!("Analyzing token: {} ({})", token.symbol, token.address); 157 | 158 | // Get trading signals from strategy engine 159 | let signals = self.strategy_engine.analyze_token(&token)?; 160 | 161 | if signals.is_empty() { 162 | log::debug!("No trading signals for token {}", token.symbol); 163 | return Ok(()); 164 | } 165 | 166 | // Execute signals 167 | for signal in signals { 168 | if signal.confidence < 0.5 { 169 | log::debug!("Low confidence signal for {}: {:.2}", token.symbol, signal.confidence); 170 | continue; 171 | } 172 | 173 | info!( 174 | "Executing {} signal for {}: {} (confidence: {:.2})", 175 | signal.action, token.symbol, signal.reason, signal.confidence 176 | ); 177 | 178 | if !self.dry_run { 179 | if let Some(trade) = self.trading_engine.execute_signal(signal).await? { 180 | self.trading_engine.add_trade(trade); 181 | info!("Trade executed successfully"); 182 | } 183 | } else { 184 | info!("DRY RUN: Would execute trade"); 185 | } 186 | } 187 | 188 | Ok(()) 189 | } 190 | 191 | async fn check_exit_conditions(&mut self) -> Result<()> { 192 | let exit_signals = self.trading_engine.check_exit_conditions().await?; 193 | 194 | for signal in exit_signals { 195 | info!("Exit condition triggered: {}", signal.reason); 196 | 197 | if !self.dry_run { 198 | if let Some(trade) = self.trading_engine.execute_signal(signal).await? { 199 | self.trading_engine.add_trade(trade); 200 | info!("Exit trade executed successfully"); 201 | } 202 | } else { 203 | info!("DRY RUN: Would execute exit trade"); 204 | } 205 | } 206 | 207 | Ok(()) 208 | } 209 | 210 | async fn update_monitoring(&mut self) { 211 | let positions = self.trading_engine.get_positions().clone(); 212 | let trades = self.trading_engine.get_trade_history().clone(); 213 | 214 | self.monitoring.update_metrics(&trades, &positions); 215 | 216 | // Check for unacknowledged alerts 217 | let unacknowledged = self.monitoring.get_unacknowledged_alerts(); 218 | for alert in unacknowledged { 219 | match alert.level { 220 | crate::monitoring::AlertLevel::Critical => { 221 | error!("CRITICAL ALERT: {}", alert.message); 222 | } 223 | crate::monitoring::AlertLevel::Error => { 224 | error!("ERROR: {}", alert.message); 225 | } 226 | crate::monitoring::AlertLevel::Warning => { 227 | warn!("WARNING: {}", alert.message); 228 | } 229 | crate::monitoring::AlertLevel::Info => { 230 | info!("INFO: {}", alert.message); 231 | } 232 | } 233 | } 234 | } 235 | 236 | async fn log_status(&self) { 237 | let metrics = self.monitoring.get_metrics(); 238 | let positions = self.trading_engine.get_positions(); 239 | 240 | info!( 241 | "Status - Trades: {}, Win Rate: {:.1}%, P&L: ${:.2}, Positions: {}, Uptime: {}s", 242 | metrics.total_trades, 243 | metrics.win_rate, 244 | metrics.total_profit_loss, 245 | positions.len(), 246 | metrics.uptime_seconds 247 | ); 248 | 249 | if !positions.is_empty() { 250 | info!("Current positions:"); 251 | for (address, position) in positions { 252 | info!( 253 | " {}: {} tokens @ ${:.6}", 254 | address, position.amount, position.entry_price 255 | ); 256 | } 257 | } 258 | } 259 | 260 | pub fn stop(&mut self) { 261 | self.running = false; 262 | } 263 | 264 | pub async fn get_balance(&self) -> Result { 265 | self.wallet.get_balance().await 266 | } 267 | 268 | pub fn get_positions(&self) -> &HashMap { 269 | self.trading_engine.get_positions() 270 | } 271 | 272 | pub fn get_metrics(&self) -> &crate::monitoring::BotMetrics { 273 | self.monitoring.get_metrics() 274 | } 275 | 276 | pub async fn save_state(&self) -> Result<()> { 277 | // Save metrics 278 | self.monitoring.save_metrics_to_file("metrics.json")?; 279 | 280 | // Save configuration 281 | self.config.save("config_backup.toml")?; 282 | 283 | info!("Bot state saved"); 284 | Ok(()) 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/trading.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{BotError, Result}; 2 | use crate::strategies::{Action, TradingSignal}; 3 | use crate::wallet::WalletManager; 4 | use rust_decimal::Decimal; 5 | use solana_sdk::{ 6 | instruction::Instruction, 7 | pubkey::Pubkey, 8 | system_instruction, 9 | transaction::Transaction, 10 | }; 11 | use spl_token::instruction as token_instruction; 12 | use std::collections::HashMap; 13 | use std::str::FromStr; 14 | 15 | #[derive(Debug, Clone)] 16 | pub struct Position { 17 | pub token_address: String, 18 | pub token_mint: Pubkey, 19 | pub amount: u64, 20 | pub entry_price: f64, 21 | pub entry_time: chrono::DateTime, 22 | pub token_account: Option, 23 | } 24 | 25 | #[derive(Debug, Clone)] 26 | pub struct Trade { 27 | pub id: String, 28 | pub token_address: String, 29 | pub action: Action, 30 | pub amount: u64, 31 | pub price: f64, 32 | pub timestamp: chrono::DateTime, 33 | pub signature: Option, 34 | pub profit_loss: Option, 35 | } 36 | 37 | pub struct TradingEngine { 38 | wallet: WalletManager, 39 | positions: HashMap, 40 | trade_history: Vec, 41 | max_positions: usize, 42 | max_buy_amount: u64, 43 | max_sell_amount: u64, 44 | profit_target_percent: f64, 45 | stop_loss_percent: f64, 46 | cooldown_seconds: u64, 47 | last_trade_time: HashMap>, 48 | } 49 | 50 | impl TradingEngine { 51 | pub fn new( 52 | wallet: &WalletManager, 53 | max_positions: usize, 54 | max_buy_amount: u64, 55 | max_sell_amount: u64, 56 | profit_target_percent: f64, 57 | stop_loss_percent: f64, 58 | cooldown_seconds: u64, 59 | ) -> Self { 60 | Self { 61 | wallet: wallet.clone(), 62 | positions: HashMap::new(), 63 | trade_history: Vec::new(), 64 | max_positions, 65 | max_buy_amount, 66 | max_sell_amount, 67 | profit_target_percent, 68 | stop_loss_percent, 69 | cooldown_seconds, 70 | last_trade_time: HashMap::new(), 71 | } 72 | } 73 | 74 | pub async fn execute_signal(&mut self, signal: TradingSignal) -> Result> { 75 | // Check cooldown 76 | if self.is_in_cooldown(&signal.token.address).await? { 77 | log::info!("Token {} is in cooldown, skipping trade", signal.token.address); 78 | return Ok(None); 79 | } 80 | 81 | match signal.action { 82 | Action::Buy => self.execute_buy(signal).await, 83 | Action::Sell => self.execute_sell(signal).await, 84 | Action::Hold => { 85 | log::debug!("Hold signal for token {}", signal.token.address); 86 | Ok(None) 87 | } 88 | } 89 | } 90 | 91 | async fn execute_buy(&mut self, signal: TradingSignal) -> Result> { 92 | // Check if we already have a position 93 | if self.positions.contains_key(&signal.token.address) { 94 | log::debug!("Already have position in token {}", signal.token.address); 95 | return Ok(None); 96 | } 97 | 98 | // Check max positions 99 | if self.positions.len() >= self.max_positions { 100 | log::warn!("Maximum positions reached, cannot buy {}", signal.token.address); 101 | return Ok(None); 102 | } 103 | 104 | // Calculate buy amount 105 | let buy_amount = self.calculate_buy_amount(&signal)?; 106 | if buy_amount == 0 { 107 | log::warn!("Buy amount is 0 for token {}", signal.token.address); 108 | return Ok(None); 109 | } 110 | 111 | // Get token mint 112 | let token_mint = Pubkey::from_str(&signal.token.address) 113 | .map_err(|e| BotError::InvalidTokenAddress(format!("Invalid token address: {}", e)))?; 114 | 115 | // Create or get token account 116 | let token_account = self.get_or_create_token_account(&token_mint).await?; 117 | 118 | // Execute buy transaction 119 | let trade = self.create_buy_trade(&signal, buy_amount, token_account).await?; 120 | 121 | // Update position 122 | let position = Position { 123 | token_address: signal.token.address.clone(), 124 | token_mint, 125 | amount: buy_amount, 126 | entry_price: signal.token.price_usd, 127 | entry_time: chrono::Utc::now(), 128 | token_account: Some(token_account), 129 | }; 130 | 131 | self.positions.insert(signal.token.address.clone(), position); 132 | self.last_trade_time.insert(signal.token.address, chrono::Utc::now()); 133 | 134 | log::info!( 135 | "Bought {} tokens of {} at ${:.6}", 136 | buy_amount, 137 | signal.token.symbol, 138 | signal.token.price_usd 139 | ); 140 | 141 | Ok(Some(trade)) 142 | } 143 | 144 | async fn execute_sell(&mut self, signal: TradingSignal) -> Result> { 145 | let position = match self.positions.get(&signal.token.address) { 146 | Some(pos) => pos, 147 | None => { 148 | log::debug!("No position found for token {}", signal.token.address); 149 | return Ok(None); 150 | } 151 | }; 152 | 153 | // Calculate sell amount 154 | let sell_amount = self.calculate_sell_amount(position)?; 155 | if sell_amount == 0 { 156 | log::warn!("Sell amount is 0 for token {}", signal.token.address); 157 | return Ok(None); 158 | } 159 | 160 | // Execute sell transaction 161 | let trade = self.create_sell_trade(&signal, sell_amount, position).await?; 162 | 163 | // Update or remove position 164 | if sell_amount >= position.amount { 165 | self.positions.remove(&signal.token.address); 166 | log::info!("Sold entire position of {}", signal.token.symbol); 167 | } else { 168 | // Partial sell - update position 169 | let mut updated_position = position.clone(); 170 | updated_position.amount -= sell_amount; 171 | self.positions.insert(signal.token.address.clone(), updated_position); 172 | log::info!("Partially sold position of {}", signal.token.symbol); 173 | } 174 | 175 | self.last_trade_time.insert(signal.token.address, chrono::Utc::now()); 176 | 177 | Ok(Some(trade)) 178 | } 179 | 180 | fn calculate_buy_amount(&self, signal: &TradingSignal) -> Result { 181 | let confidence_multiplier = signal.confidence; 182 | let base_amount = (self.max_buy_amount as f64 * confidence_multiplier) as u64; 183 | 184 | // Ensure we don't exceed max buy amount 185 | let buy_amount = base_amount.min(self.max_buy_amount); 186 | 187 | // Check if we have enough SOL balance 188 | // This is a simplified check - in practice, you'd need to account for transaction fees 189 | Ok(buy_amount) 190 | } 191 | 192 | fn calculate_sell_amount(&self, position: &Position) -> Result { 193 | // For now, sell entire position 194 | // In practice, you might want to implement partial selling strategies 195 | Ok(position.amount) 196 | } 197 | 198 | async fn get_or_create_token_account(&self, token_mint: &Pubkey) -> Result { 199 | // Check if token account already exists 200 | let token_accounts = self.wallet.get_rpc_client() 201 | .get_token_accounts_by_owner( 202 | &self.wallet.get_address(), 203 | solana_client::rpc_request::TokenAccountsFilter::Mint(*token_mint), 204 | ) 205 | .await 206 | .map_err(|e| BotError::SolanaClient(e))?; 207 | 208 | if !token_accounts.is_empty() { 209 | return Ok(token_accounts[0].pubkey); 210 | } 211 | 212 | // Create new token account 213 | self.wallet.create_token_account(token_mint).await 214 | } 215 | 216 | async fn create_buy_trade( 217 | &self, 218 | signal: &TradingSignal, 219 | amount: u64, 220 | token_account: Pubkey, 221 | ) -> Result { 222 | // This is a simplified implementation 223 | // In practice, you'd need to implement the actual swap logic 224 | // using Raydium, Jupiter, or other DEX aggregators 225 | 226 | let trade = Trade { 227 | id: uuid::Uuid::new_v4().to_string(), 228 | token_address: signal.token.address.clone(), 229 | action: Action::Buy, 230 | amount, 231 | price: signal.token.price_usd, 232 | timestamp: chrono::Utc::now(), 233 | signature: None, // Would be set after transaction confirmation 234 | profit_loss: None, 235 | }; 236 | 237 | Ok(trade) 238 | } 239 | 240 | async fn create_sell_trade( 241 | &self, 242 | signal: &TradingSignal, 243 | amount: u64, 244 | position: &Position, 245 | ) -> Result { 246 | let profit_loss = Some( 247 | (signal.token.price_usd - position.entry_price) * (amount as f64 / 1_000_000.0) 248 | ); 249 | 250 | let trade = Trade { 251 | id: uuid::Uuid::new_v4().to_string(), 252 | token_address: signal.token.address.clone(), 253 | action: Action::Sell, 254 | amount, 255 | price: signal.token.price_usd, 256 | timestamp: chrono::Utc::now(), 257 | signature: None, // Would be set after transaction confirmation 258 | profit_loss, 259 | }; 260 | 261 | Ok(trade) 262 | } 263 | 264 | async fn is_in_cooldown(&self, token_address: &str) -> Result { 265 | if let Some(last_trade) = self.last_trade_time.get(token_address) { 266 | let elapsed = chrono::Utc::now() - *last_trade; 267 | return Ok(elapsed.num_seconds() < self.cooldown_seconds as i64); 268 | } 269 | Ok(false) 270 | } 271 | 272 | pub fn get_positions(&self) -> &HashMap { 273 | &self.positions 274 | } 275 | 276 | pub fn get_trade_history(&self) -> &Vec { 277 | &self.trade_history 278 | } 279 | 280 | pub fn add_trade(&mut self, trade: Trade) { 281 | self.trade_history.push(trade); 282 | } 283 | 284 | pub async fn check_exit_conditions(&mut self) -> Result> { 285 | let mut exit_signals = Vec::new(); 286 | 287 | for (token_address, position) in &self.positions.clone() { 288 | // Check profit/loss conditions 289 | let current_price = position.entry_price; // In practice, fetch current price 290 | let profit_percent = ((current_price - position.entry_price) / position.entry_price) * 100.0; 291 | let loss_percent = ((position.entry_price - current_price) / position.entry_price) * 100.0; 292 | 293 | if profit_percent >= self.profit_target_percent || loss_percent >= self.stop_loss_percent { 294 | // Create sell signal 295 | let token_info = crate::pumpportal::TokenInfo { 296 | address: token_address.clone(), 297 | symbol: "UNKNOWN".to_string(), // Would fetch from API 298 | name: "Unknown".to_string(), 299 | decimals: 6, 300 | market_cap: 0, 301 | holders: 0, 302 | age_hours: 0, 303 | liquidity: 0, 304 | price_usd: current_price, 305 | price_change_24h: 0.0, 306 | volume_24h: 0, 307 | created_at: "".to_string(), 308 | }; 309 | 310 | let reason = if profit_percent >= self.profit_target_percent { 311 | format!("Profit target reached: {:.2}%", profit_percent) 312 | } else { 313 | format!("Stop loss triggered: {:.2}%", loss_percent) 314 | }; 315 | 316 | exit_signals.push(TradingSignal { 317 | token: token_info, 318 | action: Action::Sell, 319 | confidence: 1.0, 320 | reason, 321 | expected_price: Some(current_price), 322 | }); 323 | } 324 | } 325 | 326 | Ok(exit_signals) 327 | } 328 | } 329 | --------------------------------------------------------------------------------