From 40e4a4b13057fc38024606b86e607013e1135d7d Mon Sep 17 00:00:00 2001 From: smypmsa Date: Thu, 24 Apr 2025 20:32:47 +0000 Subject: [PATCH] feat: add support for multiple bots --- bots/bot-sniper-1-geyser.yaml | 71 +++ bots/bot-sniper-2-logs.yaml | 71 +++ pyproject.toml | 3 +- src/bot_runner.py | 152 ++++++ src/cleanup/manager.py | 10 +- src/cleanup/modes.py | 37 +- src/cli.py | 142 ----- src/config.py | 112 ---- src/config_loader.py | 188 +++++++ src/core/client.py | 5 +- src/trading/trader.py | 505 ++++++++++++------ src/utils/logger.py | 16 +- ...DeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump.txt | 9 - trades/trades.log | 14 +- 14 files changed, 875 insertions(+), 460 deletions(-) create mode 100644 bots/bot-sniper-1-geyser.yaml create mode 100644 bots/bot-sniper-2-logs.yaml create mode 100644 src/bot_runner.py delete mode 100644 src/cli.py delete mode 100644 src/config.py create mode 100644 src/config_loader.py delete mode 100644 trades/LR7TYDeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump.txt diff --git a/bots/bot-sniper-1-geyser.yaml b/bots/bot-sniper-1-geyser.yaml new file mode 100644 index 0000000..432a69e --- /dev/null +++ b/bots/bot-sniper-1-geyser.yaml @@ -0,0 +1,71 @@ +# This file defines comprehensive parameters and settings for the trading bot. +# Carefully review and adjust values to match your trading strategy and risk tolerance. + +# Bot identification and connection settings +name: "bot-sniper-1" +env_file: ".env" +rpc_endpoint: "${SOLANA_NODE_RPC_ENDPOINT}" +wss_endpoint: "${SOLANA_NODE_WSS_ENDPOINT}" +private_key: "${SOLANA_PRIVATE_KEY}" + +enabled: true # You can turn off the bot w/o removing its config +separate_process: true + +# Geyser configuration (fastest method for getting updates) +geyser: + endpoint: "${GEYSER_ENDPOINT}" + api_token: "${GEYSER_API_TOKEN}" + +# Trading parameters +# Control trade execution: amount of SOL per trade and acceptable price deviation +trade: + buy_amount: 0.0001 # Amount of SOL to spend when buying (in SOL) + buy_slippage: 0.3 # Maximum acceptable price deviation (0.3 = 30%) + sell_slippage: 0.3 + + # EXTREME FAST mode configuration + # When enabled, skips waiting for the bonding curve to stabilize and RPC price check. + # The bot buys the specified number of tokens directly, making the process faster but less precise. + extreme_fast_mode: true + extreme_fast_token_amount: 20 # Amount of tokens to buy + +# Priority fee configuration +# Manage transaction speed and cost on the Solana network. +# Note: dynamic mode requires an additional RPC call, which slows down the buying process. +priority_fees: + enable_dynamic: false # Use latest transactions to estimate required fee (getRecentPrioritizationFees) + enable_fixed: true # Use fixed amount below + fixed_amount: 1_000_000 # Base fee in microlamports + extra_percentage: 0.0 # Percentage increase on riority fee regardless of the calculation method (0.1 = 10%) + hard_cap: 1_000_000 # Maximum allowable fee in microlamports to prevent excessive spending + +# Filters for token selection +filters: + match_string: null # Only process tokens with this string in name/symbol + bro_address: null # Only trade tokens created by this user address + listener_type: "geyser" # Method for detecting new tokens: "logs", "blocks", or "geyser" + max_token_age: 0.001 # Maximum token age in seconds for processing + marry_mode: false # Only buy tokens, skip selling + yolo_mode: false # Continuously trade tokens + +# Retry and timeout settings +retries: + max_attempts: 1 # Number of attempts for transaction submission + wait_after_creation: 15 # Seconds to wait after token creation (only if EXTREME FAST is disabled) + wait_after_buy: 15 # Holding period after buy transaction + wait_before_new_token: 15 # Pause between token trades + +# Token and account management +cleanup: + # Cleanup mode determines when to manage token accounts. Options: + # "disabled": no cleanup will occur. + # "on_fail": only clean up if a buy transaction fails. + # "after_sell": clean up after selling. + # "post_session": clean up all empty accounts after a trading session ends. + mode: "post_session" + force_close_with_burn: false # Force burning remaining tokens before closing account + with_priority_fee: false # Use priority fees for cleanup transactions + +# Node provider configuration (not implemented) +node: + max_rps: 25 # Maximum requests per second diff --git a/bots/bot-sniper-2-logs.yaml b/bots/bot-sniper-2-logs.yaml new file mode 100644 index 0000000..a078df7 --- /dev/null +++ b/bots/bot-sniper-2-logs.yaml @@ -0,0 +1,71 @@ +# This file defines comprehensive parameters and settings for the trading bot. +# Carefully review and adjust values to match your trading strategy and risk tolerance. + +# Bot identification and connection settings +name: "bot-sniper-2" +env_file: ".env" +rpc_endpoint: "${SOLANA_NODE_RPC_ENDPOINT}" +wss_endpoint: "${SOLANA_NODE_WSS_ENDPOINT}" +private_key: "${SOLANA_PRIVATE_KEY}" + +enabled: false # You can turn off the bot w/o removing its config +separate_process: true + +# Geyser configuration (fastest method for getting updates) +geyser: + endpoint: "${GEYSER_ENDPOINT}" + api_token: "${GEYSER_API_TOKEN}" + +# Trading parameters +# Control trade execution: amount of SOL per trade and acceptable price deviation +trade: + buy_amount: 0.0001 # Amount of SOL to spend when buying (in SOL) + buy_slippage: 0.3 # Maximum acceptable price deviation (0.3 = 30%) + sell_slippage: 0.3 + + # EXTREME FAST mode configuration + # When enabled, skips waiting for the bonding curve to stabilize and RPC price check. + # The bot buys the specified number of tokens directly, making the process faster but less precise. + extreme_fast_mode: true + extreme_fast_token_amount: 20 # Amount of tokens to buy + +# Priority fee configuration +# Manage transaction speed and cost on the Solana network. +# Note: dynamic mode requires an additional RPC call, which slows down the buying process. +priority_fees: + enable_dynamic: false # Use latest transactions to estimate required fee (getRecentPrioritizationFees) + enable_fixed: true # Use fixed amount below + fixed_amount: 200_000 # Base fee in microlamports + extra_percentage: 0.0 # Percentage increase on riority fee regardless of the calculation method (0.1 = 10%) + hard_cap: 200_000 # Maximum allowable fee in microlamports to prevent excessive spending + +# Filters for token selection +filters: + match_string: null # Only process tokens with this string in name/symbol + bro_address: null # Only trade tokens created by this user address + listener_type: "logs" # Method for detecting new tokens: "logs", "blocks", or "geyser" + max_token_age: 0.001 # Maximum token age in seconds for processing + marry_mode: false # Only buy tokens, skip selling + yolo_mode: false # Continuously trade tokens + +# Retry and timeout settings +retries: + max_attempts: 1 # Number of attempts for transaction submission + wait_after_creation: 15 # Seconds to wait after token creation (only if EXTREME FAST is disabled) + wait_after_buy: 15 # Holding period after buy transaction + wait_before_new_token: 15 # Pause between token trades + +# Token and account management +cleanup: + # Cleanup mode determines when to manage token accounts. Options: + # "disabled": no cleanup will occur. + # "on_fail": only clean up if a buy transaction fails. + # "after_sell": clean up after selling. + # "post_session": clean up all empty accounts after a trading session ends. + mode: "post_session" + force_close_with_burn: false # Force burning remaining tokens before closing account + with_priority_fee: false # Use priority fees for cleanup transactions + +# Node provider configuration (not implemented) +node: + max_rps: 25 # Maximum requests per second diff --git a/pyproject.toml b/pyproject.toml index 02a80b8..8750aa5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ dependencies = [ "grpcio>=1.71.0", "grpcio-tools>=1.71.0", "protobuf>=5.29.4", + "pyyaml>=6.0.2", ] [project.optional-dependencies] @@ -26,7 +27,7 @@ dev = [ ] [project.scripts] -pump_bot = "cli:sync_main" +pump_bot = "bot_runner:main" [tool.ruff] exclude = [ diff --git a/src/bot_runner.py b/src/bot_runner.py new file mode 100644 index 0000000..48a7f4a --- /dev/null +++ b/src/bot_runner.py @@ -0,0 +1,152 @@ +import asyncio +import logging +import multiprocessing +from datetime import datetime +from pathlib import Path + +from config_loader import load_bot_config, print_config_summary +from trading.trader import PumpTrader +from utils.logger import setup_file_logging + + +def setup_logging(bot_name: str): + """ + Set up logging to file for a specific bot instance. + + Args: + bot_name: Name of the bot for the log file + """ + log_dir = Path("logs") + log_dir.mkdir(exist_ok=True) + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + log_filename = log_dir / f"{bot_name}_{timestamp}.log" + + setup_file_logging(str(log_filename)) + +async def start_bot(config_path: str): + """ + Start a trading bot with the configuration from the specified path. + + Args: + config_path: Path to the YAML configuration file + """ + cfg = load_bot_config(config_path) + setup_logging(cfg["name"]) + print_config_summary(cfg) + + trader = PumpTrader( + # Connection settings + rpc_endpoint=cfg["rpc_endpoint"], + wss_endpoint=cfg["wss_endpoint"], + private_key=cfg["private_key"], + + # Trade parameters + buy_amount=cfg["trade"]["buy_amount"], + buy_slippage=cfg["trade"]["buy_slippage"], + sell_slippage=cfg["trade"]["sell_slippage"], + + # Extreme fast mode settings + extreme_fast_mode=cfg["trade"].get("extreme_fast_mode", False), + extreme_fast_token_amount=cfg["trade"].get("extreme_fast_token_amount", 30), + + # Listener configuration + listener_type=cfg["filters"]["listener_type"], + + # Geyser configuration (if applicable) + geyser_endpoint=cfg.get("geyser", {}).get("endpoint"), + geyser_api_token=cfg.get("geyser", {}).get("api_token"), + + # Priority fee configuration + enable_dynamic_priority_fee=cfg.get("priority_fees", {}).get("enable_dynamic", False), + enable_fixed_priority_fee=cfg.get("priority_fees", {}).get("enable_fixed", True), + fixed_priority_fee=cfg.get("priority_fees", {}).get("fixed_amount", 500000), + extra_priority_fee=cfg.get("priority_fees", {}).get("extra_percentage", 0.0), + hard_cap_prior_fee=cfg.get("priority_fees", {}).get("hard_cap", 500000), + + # Retry and timeout settings + max_retries=cfg.get("retries", {}).get("max_attempts", 10), + wait_time_after_creation=cfg.get("retries", {}).get("wait_after_creation", 15), + wait_time_after_buy=cfg.get("retries", {}).get("wait_after_buy", 15), + wait_time_before_new_token=cfg.get("retries", {}).get("wait_before_new_token", 15), + max_token_age=cfg.get("timing", {}).get("max_token_age", 0.001), + token_wait_timeout=cfg.get("timing", {}).get("token_wait_timeout", 30), + + # Cleanup settings + cleanup_mode=cfg.get("cleanup", {}).get("mode", "disabled"), + cleanup_force_close_with_burn=cfg.get("cleanup", {}).get("force_close_with_burn", False), + cleanup_with_priority_fee=cfg.get("cleanup", {}).get("with_priority_fee", False), + + # Trading filters + match_string=cfg["filters"].get("match_string"), + bro_address=cfg["filters"].get("bro_address"), + marry_mode=cfg["filters"].get("marry_mode", False), + yolo_mode=cfg["filters"].get("yolo_mode", False), + ) + + await trader.start() + +def run_all_bots(): + """ + Run all bots defined in YAML files in the 'bots' directory. + Only runs bots that have enabled=True (or where enabled is not specified). + Bots can be run in separate processes based on their configuration. + """ + bot_dir = Path("bots") + if not bot_dir.exists(): + logging.error(f"Bot directory '{bot_dir}' not found") + return + + bot_files = list(bot_dir.glob("*.yaml")) + if not bot_files: + logging.error(f"No bot configuration files found in '{bot_dir}'") + return + + logging.info(f"Found {len(bot_files)} bot configuration files") + + processes = [] + skipped_bots = 0 + + for file in bot_files: + try: + cfg = load_bot_config(str(file)) + bot_name = cfg.get("name", file.stem) + + # Skip bots with enabled=False + if not cfg.get("enabled", True): + logging.info(f"Skipping disabled bot '{bot_name}'") + skipped_bots += 1 + continue + + if cfg.get("separate_process", False): + logging.info(f"Starting bot '{bot_name}' in separate process") + p = multiprocessing.Process( + target=lambda path=str(file): asyncio.run(start_bot(path)), + name=f"bot-{bot_name}" + ) + p.start() + processes.append(p) + else: + logging.info(f"Starting bot '{bot_name}' in main process") + asyncio.run(start_bot(str(file))) + except Exception as e: + logging.exception(f"Failed to start bot from {file}: {e}") + + logging.info(f"Started {len(bot_files) - skipped_bots} bots, skipped {skipped_bots} disabled bots") + + for p in processes: + p.join() + logging.info(f"Process {p.name} completed") + + +def main() -> None: + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + ) + + run_all_bots() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/cleanup/manager.py b/src/cleanup/manager.py index 9bcb8dc..2d4dca8 100644 --- a/src/cleanup/manager.py +++ b/src/cleanup/manager.py @@ -1,7 +1,8 @@ +import asyncio + from solders.pubkey import Pubkey from spl.token.instructions import BurnParams, CloseAccountParams, burn, close_account -from config import CLEANUP_FORCE_CLOSE_WITH_BURN from core.client import SolanaClient from core.priority_fee.manager import PriorityFeeManager from core.pubkeys import SystemAddresses @@ -19,6 +20,7 @@ class AccountCleanupManager: wallet: Wallet, priority_fee_manager: PriorityFeeManager, use_priority_fee: bool = False, + force_burn: bool = False, ): """ Args: @@ -29,6 +31,7 @@ class AccountCleanupManager: self.wallet = wallet self.priority_fee_manager = priority_fee_manager self.use_priority_fee = use_priority_fee + self.close_with_force_burn = force_burn async def cleanup_ata(self, mint: Pubkey) -> None: """ @@ -44,6 +47,9 @@ class AccountCleanupManager: else None ) + logger.info("Waiting for 15 seconds for RPC node to synchronize...") + await asyncio.sleep(15) + try: info = await solana_client.get_account_info(ata, encoding="base64") if not info.value: @@ -53,7 +59,7 @@ class AccountCleanupManager: balance = await self.client.get_token_account_balance(ata) instructions = [] - if balance > 0 and CLEANUP_FORCE_CLOSE_WITH_BURN: + if balance > 0 and self.close_with_force_burn: logger.info(f"Burning {balance} tokens from ATA {ata} (mint: {mint})...") burn_ix = burn( BurnParams( diff --git a/src/cleanup/modes.py b/src/cleanup/modes.py index 8c11794..428f3b4 100644 --- a/src/cleanup/modes.py +++ b/src/cleanup/modes.py @@ -1,37 +1,42 @@ from cleanup.manager import AccountCleanupManager -from config import CLEANUP_MODE, CLEANUP_WITH_PRIORITY_FEE from utils.logger import get_logger logger = get_logger(__name__) -def should_cleanup_after_failure() -> bool: - return CLEANUP_MODE == "on_fail" +def should_cleanup_after_failure(cleanup_mode) -> bool: + return cleanup_mode == "on_fail" -def should_cleanup_after_sell() -> bool: - return CLEANUP_MODE == "after_sell" +def should_cleanup_after_sell(cleanup_mode) -> bool: + return cleanup_mode == "after_sell" -def should_cleanup_post_session() -> bool: - return CLEANUP_MODE == "post_session" +def should_cleanup_post_session(cleanup_mode) -> bool: + return cleanup_mode == "post_session" -async def handle_cleanup_after_failure(client, wallet, mint, priority_fee_manager): - if should_cleanup_after_failure(): +async def handle_cleanup_after_failure( + client, wallet, mint, priority_fee_manager, cleanup_mode, cleanup_with_prior_fee, force_burn + ): + if should_cleanup_after_failure(cleanup_mode): logger.info("[Cleanup] Triggered by failed buy transaction.") - manager = AccountCleanupManager(client, wallet, priority_fee_manager, CLEANUP_WITH_PRIORITY_FEE) + manager = AccountCleanupManager(client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn) await manager.cleanup_ata(mint) -async def handle_cleanup_after_sell(client, wallet, mint, priority_fee_manager): - if should_cleanup_after_sell(): +async def handle_cleanup_after_sell( + client, wallet, mint, priority_fee_manager, cleanup_mode, cleanup_with_prior_fee, force_burn + ): + if should_cleanup_after_sell(cleanup_mode): logger.info("[Cleanup] Triggered after token sell.") - manager = AccountCleanupManager(client, wallet, priority_fee_manager, CLEANUP_WITH_PRIORITY_FEE) + manager = AccountCleanupManager(client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn) await manager.cleanup_ata(mint) -async def handle_cleanup_post_session(client, wallet, mints, priority_fee_manager): - if should_cleanup_post_session(): +async def handle_cleanup_post_session( + client, wallet, mints, priority_fee_manager, cleanup_mode, cleanup_with_prior_fee, force_burn + ): + if should_cleanup_post_session(cleanup_mode): logger.info("[Cleanup] Triggered post trading session.") - manager = AccountCleanupManager(client, wallet, priority_fee_manager, CLEANUP_WITH_PRIORITY_FEE) + manager = AccountCleanupManager(client, wallet, priority_fee_manager, cleanup_with_prior_fee, force_burn) for mint in mints: await manager.cleanup_ata(mint) diff --git a/src/cli.py b/src/cli.py deleted file mode 100644 index 7d7d5ee..0000000 --- a/src/cli.py +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/env python3 -""" -Command-line interface for the pump.fun trading bot. -""" - -import argparse -import asyncio -import os -import sys - -from dotenv import load_dotenv - -import config -from trading.trader import PumpTrader -from utils.logger import get_logger, setup_file_logging - -load_dotenv() - -logger = get_logger(__name__) - - -def parse_args() -> argparse.Namespace: - """Parse command line arguments. - - Returns: - Parsed arguments - """ - parser = argparse.ArgumentParser(description="Trade tokens on pump.fun.") - parser.add_argument( - "--yolo", action="store_true", help="Run in YOLO mode (continuous trading)" - ) - parser.add_argument( - "--match", - type=str, - help="Only trade tokens with names or symbols matching this string", - ) - parser.add_argument( - "--bro", type=str, help="Only trade tokens created by this user address" - ) - parser.add_argument( - "--marry", action="store_true", help="Only buy tokens, skip selling" - ) - parser.add_argument( - "--amount", - type=float, - help=f"Amount of SOL to spend on each buy (default: {config.BUY_AMOUNT})", - ) - parser.add_argument( - "--buy-slippage", - type=float, - help=f"Buy slippage tolerance (default: {config.BUY_SLIPPAGE})", - ) - parser.add_argument( - "--sell-slippage", - type=float, - help=f"Sell slippage tolerance (default: {config.SELL_SLIPPAGE})", - ) - - return parser.parse_args() - - -async def main() -> None: - """Main entry point for the CLI.""" - setup_file_logging("pump_trading.log") - - args = parse_args() - - # Get configuration values, preferring command line args over config.py - rpc_endpoint: str | None = os.environ.get("SOLANA_NODE_RPC_ENDPOINT") - wss_endpoint: str | None = os.environ.get("SOLANA_NODE_WSS_ENDPOINT") - private_key: str | None = os.environ.get("SOLANA_PRIVATE_KEY") - geyser_endpoint: str | None = os.environ.get("GEYSER_ENDPOINT") - geyser_api_token: str | None = os.environ.get("GEYSER_API_TOKEN") - - - # Validate configuration values - if not rpc_endpoint or not rpc_endpoint.startswith(("http://", "https://")): - logger.error("Invalid RPC endpoint. Must start with http:// or https://") - sys.exit(1) - - if not wss_endpoint or not wss_endpoint.startswith(("ws://", "wss://")): - logger.error("Invalid WebSocket endpoint. Must start with ws:// or wss://") - sys.exit(1) - - if not private_key or len(private_key) < 80: - logger.error("Invalid private key. Key appears to be missing or too short") - sys.exit(1) - - if config.LISTENER_TYPE.lower() == "geyser": - if not geyser_endpoint: - logger.error("GEYSER_ENDPOINT environment variable is not set") - sys.exit(1) - if not geyser_api_token: - logger.error("GEYSER_API_TOKEN environment variable is not set") - sys.exit(1) - - # Get trading parameters - buy_amount: float = args.amount if args.amount is not None else config.BUY_AMOUNT - buy_slippage: float = ( - args.buy_slippage if args.buy_slippage is not None else config.BUY_SLIPPAGE - ) - sell_slippage: float = ( - args.sell_slippage if args.sell_slippage is not None else config.SELL_SLIPPAGE - ) - - trader: PumpTrader = PumpTrader( - rpc_endpoint=rpc_endpoint, # type: ignore - wss_endpoint=wss_endpoint, # type: ignore - private_key=private_key, - buy_amount=buy_amount, - buy_slippage=buy_slippage, - sell_slippage=sell_slippage, - max_retries=config.MAX_RETRIES, - listener_type=config.LISTENER_TYPE, - geyser_endpoint=geyser_endpoint, - geyser_api_token=geyser_api_token, - ) - - try: - await trader.start( - match_string=args.match, - bro_address=args.bro, - marry_mode=args.marry, - yolo_mode=args.yolo, - ) - except KeyboardInterrupt: - logger.info("Trading stopped by user") - except Exception as e: - logger.error(f"Trading stopped due to error: {e!s}") - finally: - try: - await trader.solana_client.close() - except Exception: - pass - - -def sync_main() -> None: - asyncio.run(main()) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/src/config.py b/src/config.py deleted file mode 100644 index 70497bd..0000000 --- a/src/config.py +++ /dev/null @@ -1,112 +0,0 @@ -""" -Configuration for the pump.fun trading bot. - -This file defines comprehensive parameters and settings for the trading bot. -Carefully review and adjust values to match your trading strategy and risk tolerance. -""" - -# Trading parameters -# Control trade execution: amount of SOL per trade and acceptable price deviation -BUY_AMOUNT: int | float = 0.000_001 # Amount of SOL to spend when buying -BUY_SLIPPAGE: float = 0.4 # Maximum acceptable price deviation (0.4 = 40%) -SELL_SLIPPAGE: float = 0.4 # Consistent slippage tolerance to maintain trading strategy - - -# EXTREME FAST Mode configuration -# When enabled, skips waiting for the bonding curve to stabilize and RPC price check. -# The bot buys the specified number of tokens directly, making the process faster but less precise. -EXTREME_FAST_MODE: bool = True -# Amount of tokens to buy in EXTREME FAST mode. No price calculation is done; the bot buys exactly this amount. -EXTREME_FAST_TOKEN_AMOUNT: int = 30 - - -# Priority fee configuration -# Manage transaction speed and cost on the Solana network -ENABLE_DYNAMIC_PRIORITY_FEE: bool = False # Adaptive fee calculation -ENABLE_FIXED_PRIORITY_FEE: bool = True # Use consistent, predictable fee -FIXED_PRIORITY_FEE: int = 500_000 # Base fee in microlamports -EXTRA_PRIORITY_FEE: float = 0.0 # Percentage increase on priority fee (0.1 = 10%) -HARD_CAP_PRIOR_FEE: int = 500_000 # Maximum allowable fee to prevent excessive spending in microlamports - - -# Listener configuration -# Choose method for detecting new tokens on the network -# "logs": Recommended for more stable token detection -# "blocks": Unstable method, potentially less reliable -# "geyser": The fastest way for getting updates, requires Geyser endpoint -LISTENER_TYPE = "logs" - - -# Retry and timeout settings -# Control bot resilience and transaction handling -MAX_RETRIES: int = 10 # Number of attempts for transaction submission - -# Waiting periods in seconds between actions (TODO: to be replaced with retry mechanism) -WAIT_TIME_AFTER_CREATION: int | float = 15 # Seconds to wait after token creation -WAIT_TIME_AFTER_BUY: int | float = 15 # Holding period after buy transaction -WAIT_TIME_BEFORE_NEW_TOKEN: int | float = 15 # Pause between token trades - - -# Token and account management -# Control token processing and account cleanup strategies -MAX_TOKEN_AGE: int | float = 0.001 # Maximum token age in seconds for processing - -# Cleanup mode determines when to manage token accounts. Options: -# "disabled": No cleanup will occur. -# "on_fail": Only clean up if a buy transaction fails. -# "after_sell": Clean up after selling, but only if the balance is zero. -# "post_session": Clean up all empty accounts after a trading session ends. -CLEANUP_MODE: str = "disabled" -CLEANUP_FORCE_CLOSE_WITH_BURN: bool = False # Burn remaining tokens before closing account, else skip ATA with non-zero balances -CLEANUP_WITH_PRIORITY_FEE: bool = False # Use priority fees for cleanup transactions - - -# Node provider configuration (TODO: to be implemented) -# Manage RPC node interaction to prevent rate limiting -MAX_RPS: int = 25 # Maximum requests per second - - -def validate_configuration() -> None: - """ - Comprehensive validation of bot configuration. - - Checks: - - Type correctness - - Value ranges - - Logical consistency of settings - """ - # Configuration validation checks - config_checks = [ - # (value, type, min_value, max_value, error_message) - (BUY_AMOUNT, (int, float), 0, float('inf'), "BUY_AMOUNT must be a positive number"), - (BUY_SLIPPAGE, float, 0, 1, "BUY_SLIPPAGE must be between 0 and 1"), - (SELL_SLIPPAGE, float, 0, 1, "SELL_SLIPPAGE must be between 0 and 1"), - (FIXED_PRIORITY_FEE, int, 0, float('inf'), "FIXED_PRIORITY_FEE must be a non-negative integer"), - (EXTRA_PRIORITY_FEE, float, 0, 1, "EXTRA_PRIORITY_FEE must be between 0 and 1"), - (HARD_CAP_PRIOR_FEE, int, 0, float('inf'), "HARD_CAP_PRIOR_FEE must be a non-negative integer"), - (MAX_RETRIES, int, 0, 100, "MAX_RETRIES must be between 0 and 100") - ] - - for value, expected_type, min_val, max_val, error_msg in config_checks: - if not isinstance(value, expected_type): - raise ValueError(f"Type error: {error_msg}") - - if isinstance(value, (int, float)) and not (min_val <= value <= max_val): - raise ValueError(f"Range error: {error_msg}") - - # Logical consistency checks - if ENABLE_DYNAMIC_PRIORITY_FEE and ENABLE_FIXED_PRIORITY_FEE: - raise ValueError("Cannot enable both dynamic and fixed priority fees simultaneously") - - # Validate listener type - if LISTENER_TYPE not in ["logs", "blocks", "geyser"]: - raise ValueError("LISTENER_TYPE must be either 'logs' or 'blocks'") - - # Validate cleanup mode - valid_cleanup_modes = ["disabled", "on_fail", "after_sell", "post_session"] - if CLEANUP_MODE not in valid_cleanup_modes: - raise ValueError(f"CLEANUP_MODE must be one of {valid_cleanup_modes}") - - -# Validate configuration on import -validate_configuration() \ No newline at end of file diff --git a/src/config_loader.py b/src/config_loader.py new file mode 100644 index 0000000..65b80f3 --- /dev/null +++ b/src/config_loader.py @@ -0,0 +1,188 @@ +import os +from typing import Any + +import yaml +from dotenv import load_dotenv + +REQUIRED_FIELDS = [ + "name", "rpc_endpoint", "wss_endpoint", "private_key", + "trade.buy_amount", "trade.buy_slippage", "trade.sell_slippage", + "filters.listener_type", "filters.max_token_age" +] + +CONFIG_VALIDATION_RULES = [ + # (path, type, min_value, max_value, error_message) + ("trade.buy_amount", (int, float), 0, float('inf'), "trade.buy_amount must be a positive number"), + ("trade.buy_slippage", float, 0, 1, "trade.buy_slippage must be between 0 and 1"), + ("trade.sell_slippage", float, 0, 1, "trade.sell_slippage must be between 0 and 1"), + ("priority_fees.fixed_amount", int, 0, float('inf'), "priority_fees.fixed_amount must be a non-negative integer"), + ("priority_fees.extra_percentage", float, 0, 1, "priority_fees.extra_percentage must be between 0 and 1"), + ("priority_fees.hard_cap", int, 0, float('inf'), "priority_fees.hard_cap must be a non-negative integer"), + ("retries.max_attempts", int, 0, 100, "retries.max_attempts must be between 0 and 100"), + ("filters.max_token_age", (int, float), 0, float('inf'), "filters.max_token_age must be a non-negative number") +] + +# Valid values for enum-like fields +VALID_VALUES = { + "filters.listener_type": ["logs", "blocks", "geyser"], + "cleanup.mode": ["disabled", "on_fail", "after_sell", "post_session"] +} + + +def load_bot_config(path: str) -> dict: + """ + Load and validate a bot configuration from a YAML file. + + Args: + path: Path to the YAML configuration file (relative or absolute) + + Returns: + Validated configuration dictionary + + Raises: + FileNotFoundError: If the configuration file doesn't exist + ValueError: If the configuration is invalid + """ + with open(path) as f: + config = yaml.safe_load(f) + + env_file = config.get("env_file") + if env_file: + env_path = os.path.join(os.path.dirname(path), env_file) + if os.path.exists(env_path): + load_dotenv(env_path) + else: + # If not found relative to config, try relative to current working directory + load_dotenv(env_file) + + resolve_env_vars(config) + validate_config(config) + + return config + +def resolve_env_vars(config: dict) -> None: + """ + Recursively resolve environment variables in the configuration. + + Args: + config: Configuration dictionary to process + """ + def resolve_env(value): + if isinstance(value, str) and value.startswith("${") and value.endswith("}"): + env_var = value[2:-1] + env_value = os.getenv(env_var) + if env_value is None: + raise ValueError(f"Environment variable '{env_var}' not found") + return env_value + return value + + def resolve_all(d): + for k, v in d.items(): + if isinstance(v, dict): + resolve_all(v) + else: + d[k] = resolve_env(v) + + resolve_all(config) + +def get_nested_value(config: dict, path: str) -> Any: + """ + Get a nested value from the configuration using dot notation. + + Args: + config: Configuration dictionary + path: Path to the value using dot notation (e.g., "trade.buy_amount") + + Returns: + The value at the specified path + + Raises: + ValueError: If the path doesn't exist in the configuration + """ + keys = path.split(".") + value = config + for key in keys: + if not isinstance(value, dict) or key not in value: + raise ValueError(f"Missing required config key: {path}") + value = value[key] + return value + +def validate_config(config: dict) -> None: + """ + Validate the configuration against defined rules. + + Args: + config: Configuration dictionary to validate + + Raises: + ValueError: If the configuration is invalid + """ + for field in REQUIRED_FIELDS: + get_nested_value(config, field) + + for path, expected_type, min_val, max_val, error_msg in CONFIG_VALIDATION_RULES: + try: + value = get_nested_value(config, path) + + if not isinstance(value, expected_type): + raise ValueError(f"Type error: {error_msg}") + + if isinstance(value, (int, float)) and not (min_val <= value <= max_val): + raise ValueError(f"Range error: {error_msg}") + + except ValueError as e: + # Re-raise if it's our own error + if str(e).startswith(("Type error:", "Range error:")): + raise + # Otherwise, the field might be missing + continue + + # Validate enum-like fields + for path, valid_values in VALID_VALUES.items(): + try: + value = get_nested_value(config, path) + if value not in valid_values: + raise ValueError(f"{path} must be one of {valid_values}") + except ValueError: + # Skip if the field is missing + continue + + # Cannot enable both dynamic and fixed priority fees + try: + dynamic = get_nested_value(config, "priority_fees.enable_dynamic") + fixed = get_nested_value(config, "priority_fees.enable_fixed") + if dynamic and fixed: + raise ValueError("Cannot enable both dynamic and fixed priority fees simultaneously") + except ValueError: + # Skip if one of the fields is missing + pass + +def print_config_summary(config: dict) -> None: + """ + Print a summary of the loaded configuration. + + Args: + config: Configuration dictionary + """ + print(f"Bot name: {config.get('name', 'unnamed')}") + print(f"Listener type: {config.get('filters', {}).get('listener_type', 'not configured')}") + + trade = config.get('trade', {}) + print("Trade settings:") + print(f" - Buy amount: {trade.get('buy_amount', 'not configured')} SOL") + print(f" - Buy slippage: {trade.get('buy_slippage', 'not configured') * 100}%") + print(f" - Extreme fast mode: {'enabled' if trade.get('extreme_fast_mode') else 'disabled'}") + + fees = config.get('priority_fees', {}) + print("Priority fees:") + if fees.get('enable_dynamic'): + print(" - Dynamic fees enabled") + elif fees.get('enable_fixed'): + print(f" - Fixed fee: {fees.get('fixed_amount', 'not configured')} microlamports") + + print("Configuration loaded successfully!") + + +if __name__ == "__main__": + config = load_bot_config("bots/bot-sniper.yaml") + print_config_summary(config) \ No newline at end of file diff --git a/src/core/client.py b/src/core/client.py index d0caa16..4193064 100644 --- a/src/core/client.py +++ b/src/core/client.py @@ -47,7 +47,8 @@ class SolanaClient: self._cached_blockhash = blockhash except Exception as e: logger.warning(f"Blockhash fetch failed: {e!s}") - await asyncio.sleep(interval) + finally: + await asyncio.sleep(interval) async def get_cached_blockhash(self) -> Hash: """Return the most recently cached blockhash.""" @@ -195,7 +196,7 @@ class SolanaClient: """ client = await self.get_client() try: - await client.confirm_transaction(signature, commitment=commitment) + await client.confirm_transaction(signature, commitment=commitment, sleep_seconds=1) return True except Exception as e: logger.error(f"Failed to confirm transaction {signature}: {e!s}") diff --git a/src/trading/trader.py b/src/trading/trader.py index a690c55..b69aa5f 100644 --- a/src/trading/trader.py +++ b/src/trading/trader.py @@ -10,7 +10,6 @@ from datetime import datetime from solders.pubkey import Pubkey -import config from cleanup.modes import ( handle_cleanup_after_failure, handle_cleanup_after_sell, @@ -34,7 +33,6 @@ logger = get_logger(__name__) class PumpTrader: """Coordinates trading operations for pump.fun tokens with focus on freshness.""" - def __init__( self, rpc_endpoint: str, @@ -43,13 +41,40 @@ class PumpTrader: buy_amount: float, buy_slippage: float, sell_slippage: float, - max_retries: int = 5, listener_type: str = "logs", geyser_endpoint: str | None = None, geyser_api_token: str | None = None, + + extreme_fast_mode: bool = False, + extreme_fast_token_amount: int = 30, + + # Priority fee configuration + enable_dynamic_priority_fee: bool = False, + enable_fixed_priority_fee: bool = True, + fixed_priority_fee: int = 200_000, + extra_priority_fee: float = 0.0, + hard_cap_prior_fee: int = 200_000, + + # Retry and timeout settings + max_retries: int = 3, + wait_time_after_creation: int = 15, # here and further - seconds + wait_time_after_buy: int = 15, + wait_time_before_new_token: int = 15, + max_token_age: int | float = 0.001, + token_wait_timeout: int = 30, + + # Cleanup settings + cleanup_mode: str = "disabled", + cleanup_force_close_with_burn: bool = False, + cleanup_with_priority_fee: bool = False, + + # Trading filters + match_string: str | None = None, + bro_address: str | None = None, + marry_mode: bool = False, + yolo_mode: bool = False, ): """Initialize the pump trader. - Args: rpc_endpoint: RPC endpoint URL wss_endpoint: WebSocket endpoint URL @@ -57,24 +82,47 @@ class PumpTrader: buy_amount: Amount of SOL to spend on buys buy_slippage: Slippage tolerance for buys sell_slippage: Slippage tolerance for sells - max_retries: Maximum number of retry attempts + listener_type: Type of listener to use ('logs', 'blocks', or 'geyser') geyser_endpoint: Geyser endpoint URL (required for geyser listener) geyser_api_token: Geyser API token (required for geyser listener) + + extreme_fast_mode: Whether to enable extreme fast mode + extreme_fast_token_amount: Maximum token amount for extreme fast mode + + enable_dynamic_priority_fee: Whether to enable dynamic priority fees + enable_fixed_priority_fee: Whether to enable fixed priority fees + fixed_priority_fee: Fixed priority fee amount + extra_priority_fee: Extra percentage for priority fees + hard_cap_prior_fee: Hard cap for priority fees + + max_retries: Maximum number of retry attempts + wait_time_after_creation: Time to wait after token creation (seconds) + wait_time_after_buy: Time to wait after buying a token (seconds) + wait_time_before_new_token: Time to wait before processing a new token (seconds) + max_token_age: Maximum age of token to process (seconds) + token_wait_timeout: Timeout for waiting for a token in single-token mode (seconds) + + cleanup_mode: Cleanup mode ("disabled", "auto", or "manual") + cleanup_force_close_with_burn: Whether to force close with burn during cleanup + cleanup_with_priority_fee: Whether to use priority fees during cleanup + + match_string: Optional string to match in token name/symbol + bro_address: Optional creator address to filter by + marry_mode: If True, only buy tokens and skip selling + yolo_mode: If True, trade continuously """ self.solana_client = SolanaClient(rpc_endpoint) self.wallet = Wallet(private_key) self.curve_manager = BondingCurveManager(self.solana_client) - self.priority_fee_manager = PriorityFeeManager( client=self.solana_client, - enable_dynamic_fee=config.ENABLE_DYNAMIC_PRIORITY_FEE, - enable_fixed_fee=config.ENABLE_FIXED_PRIORITY_FEE, - fixed_fee=config.FIXED_PRIORITY_FEE, - extra_fee=config.EXTRA_PRIORITY_FEE, - hard_cap=config.HARD_CAP_PRIOR_FEE, + enable_dynamic_fee=enable_dynamic_priority_fee, + enable_fixed_fee=enable_fixed_priority_fee, + fixed_fee=fixed_priority_fee, + extra_fee=extra_priority_fee, + hard_cap=hard_cap_prior_fee, ) - self.buyer = TokenBuyer( self.solana_client, self.wallet, @@ -83,10 +131,9 @@ class PumpTrader: buy_amount, buy_slippage, max_retries, - config.EXTREME_FAST_TOKEN_AMOUNT, - config.EXTREME_FAST_MODE + extreme_fast_token_amount, + extreme_fast_mode ) - self.seller = TokenSeller( self.solana_client, self.wallet, @@ -95,7 +142,7 @@ class PumpTrader: sell_slippage, max_retries, ) - + # Initialize the appropriate listener type listener_type = listener_type.lower() if listener_type == "geyser": @@ -114,69 +161,166 @@ class PumpTrader: else: self.token_listener = BlockListener(wss_endpoint, PumpAddresses.PROGRAM) logger.info("Using blockSubscribe listener for token monitoring") - + + # Trading parameters self.buy_amount = buy_amount self.buy_slippage = buy_slippage self.sell_slippage = sell_slippage self.max_retries = max_retries - self.max_token_age = config.MAX_TOKEN_AGE + self.extreme_fast_mode = extreme_fast_mode + self.extreme_fast_token_amount = extreme_fast_token_amount + + # Timing parameters + self.wait_time_after_creation = wait_time_after_creation + self.wait_time_after_buy = wait_time_after_buy + self.wait_time_before_new_token = wait_time_before_new_token + self.max_token_age = max_token_age + self.token_wait_timeout = token_wait_timeout + + # Cleanup parameters + self.cleanup_mode = cleanup_mode + self.cleanup_force_close_with_burn = cleanup_force_close_with_burn + self.cleanup_with_priority_fee = cleanup_with_priority_fee + # Trading filters/modes + self.match_string = match_string + self.bro_address = bro_address + self.marry_mode = marry_mode + self.yolo_mode = yolo_mode + + # State tracking self.traded_mints: set[Pubkey] = set() - - # Token processing state - self.token_queue = asyncio.Queue() - self.processing = False + self.token_queue: asyncio.Queue = asyncio.Queue() + self.processing: bool = False self.processed_tokens: set[str] = set() self.token_timestamps: dict[str, float] = {} - async def start( - self, - match_string: str | None = None, - bro_address: str | None = None, - marry_mode: bool = False, - yolo_mode: bool = False, - ) -> None: - """Start the trading bot. - - Args: - match_string: Optional string to match in token name/symbol - bro_address: Optional creator address to filter by - marry_mode: If True, only buy tokens and skip selling - yolo_mode: If True, trade continuously - """ + async def start(self) -> None: + """Start the trading bot and listen for new tokens.""" logger.info("Starting pump.fun trader") - logger.info(f"Match filter: {match_string if match_string else 'None'}") - logger.info(f"Creator filter: {bro_address if bro_address else 'None'}") - logger.info(f"Marry mode: {marry_mode}") - logger.info(f"YOLO mode: {yolo_mode}") + logger.info(f"Match filter: {self.match_string if self.match_string else 'None'}") + logger.info(f"Creator filter: {self.bro_address if self.bro_address else 'None'}") + logger.info(f"Marry mode: {self.marry_mode}") + logger.info(f"YOLO mode: {self.yolo_mode}") logger.info(f"Max token age: {self.max_token_age} seconds") - # Start processor task - processor_task = asyncio.create_task( - self._process_token_queue(marry_mode, yolo_mode) - ) - try: - await self.token_listener.listen_for_tokens( - lambda token: self._queue_token(token), - match_string, - bro_address, - ) + # Choose operating mode based on yolo_mode + if not self.yolo_mode: + # Single token mode: process one token and exit + logger.info("Running in single token mode - will process one token and exit") + token_info = await self._wait_for_token() + if token_info: + await self._handle_token(token_info) + logger.info("Finished processing single token. Exiting...") + else: + logger.info(f"No suitable token found within timeout period ({self.token_wait_timeout}s). Exiting...") + else: + # Continuous mode: process tokens until interrupted + logger.info("Running in continuous mode - will process tokens until interrupted") + processor_task = asyncio.create_task( + self._process_token_queue() + ) + try: + await self.token_listener.listen_for_tokens( + lambda token: self._queue_token(token), + self.match_string, + self.bro_address, + ) + except Exception as e: + logger.error(f"Token listening stopped due to error: {e!s}") + finally: + processor_task.cancel() + try: + await processor_task + except asyncio.CancelledError: + pass + except Exception as e: logger.error(f"Trading stopped due to error: {e!s}") - processor_task.cancel() - await self.solana_client.close() - + finally: - processor_task.cancel() - if self.traded_mints: - # Close ATA if enabled - await handle_cleanup_post_session(self.solana_client, self.wallet, list(self.traded_mints), self.priority_fee_manager) - await self.solana_client.close() + await self._cleanup_resources() + logger.info("Pump trader has shut down") - async def _queue_token(self, token_info: TokenInfo) -> None: - """Queue a token for processing if not already processed.""" + async def _wait_for_token(self) -> TokenInfo | None: + """Wait for a single token to be detected. + + Returns: + TokenInfo or None if timeout occurs + """ + # Create a one-time event to signal when a token is found + token_found = asyncio.Event() + found_token = None + + async def token_callback(token: TokenInfo) -> None: + nonlocal found_token + token_key = str(token.mint) + + # Only process if not already processed and fresh + if token_key not in self.processed_tokens: + # Record when the token was discovered + self.token_timestamps[token_key] = asyncio.get_event_loop().time() + found_token = token + self.processed_tokens.add(token_key) + token_found.set() + + listener_task = asyncio.create_task( + self.token_listener.listen_for_tokens( + token_callback, + self.match_string, + self.bro_address, + ) + ) + + # Wait for a token with a timeout + try: + logger.info(f"Waiting for a suitable token (timeout: {self.token_wait_timeout}s)...") + await asyncio.wait_for(token_found.wait(), timeout=self.token_wait_timeout) + logger.info(f"Found token: {found_token.symbol} ({found_token.mint})") + return found_token + except TimeoutError: + logger.info(f"Timed out after waiting {self.token_wait_timeout}s for a token") + return None + finally: + listener_task.cancel() + try: + await listener_task + except asyncio.CancelledError: + pass + + async def _cleanup_resources(self) -> None: + """Perform cleanup operations before shutting down.""" + if self.traded_mints: + try: + logger.info(f"Cleaning up {len(self.traded_mints)} traded token(s)...") + await handle_cleanup_post_session( + self.solana_client, + self.wallet, + list(self.traded_mints), + self.priority_fee_manager, + self.cleanup_mode, + self.cleanup_with_priority_fee, + self.cleanup_force_close_with_burn + ) + except Exception as e: + logger.error(f"Error during cleanup: {e!s}") + + old_keys = {k for k in self.token_timestamps if k not in self.processed_tokens} + for key in old_keys: + self.token_timestamps.pop(key, None) + + await self.solana_client.close() + + async def _queue_token( + self, token_info: TokenInfo + ) -> None: + """Queue a token for processing if not already processed. + + Args: + token_info: Token information to queue + """ token_key = str(token_info.mint) if token_key in self.processed_tokens: @@ -189,126 +333,178 @@ class PumpTrader: await self.token_queue.put(token_info) logger.info(f"Queued new token: {token_info.symbol} ({token_info.mint})") - async def _process_token_queue(self, marry_mode: bool, yolo_mode: bool) -> None: + async def _process_token_queue(self) -> None: """Continuously process tokens from the queue, only if they're fresh.""" while True: - token_info = await self.token_queue.get() - token_key = str(token_info.mint) + try: + token_info = await self.token_queue.get() + token_key = str(token_info.mint) - # Check if token is still "fresh" - current_time = asyncio.get_event_loop().time() - token_age = current_time - self.token_timestamps.get( - token_key, current_time - ) - - if token_age > self.max_token_age: - logger.info( - f"Skipping token {token_info.symbol} - too old ({token_age:.1f}s > {self.max_token_age}s)" + # Check if token is still "fresh" + current_time = asyncio.get_event_loop().time() + token_age = current_time - self.token_timestamps.get( + token_key, current_time ) + + if token_age > self.max_token_age: + logger.info( + f"Skipping token {token_info.symbol} - too old ({token_age:.1f}s > {self.max_token_age}s)" + ) + #self.token_queue.task_done() + continue + + self.processed_tokens.add(token_key) + + logger.info( + f"Processing fresh token: {token_info.symbol} (age: {token_age:.1f}s)" + ) + await self._handle_token(token_info) + + except asyncio.CancelledError: + # Handle cancellation gracefully + logger.info("Token queue processor was cancelled") + break + except Exception as e: + logger.error(f"Error in token queue processor: {e!s}") + finally: self.token_queue.task_done() - continue - - self.processed_tokens.add(token_key) - - logger.info( - f"Processing fresh token: {token_info.symbol} (age: {token_age:.1f}s)" - ) - await self._handle_token(token_info, marry_mode, yolo_mode) - - self.token_queue.task_done() async def _handle_token( - self, token_info: TokenInfo, marry_mode: bool, yolo_mode: bool + self, token_info: TokenInfo ) -> None: """Handle a new token creation event. Args: token_info: Token information - marry_mode: If True, only buy tokens and skip selling - yolo_mode: If True, continue trading after this token """ try: + # Save token info to file await self._save_token_info(token_info) - if not config.EXTREME_FAST_MODE: + # Wait for bonding curve to stabilize (unless in extreme fast mode) + if not self.extreme_fast_mode: logger.info( - f"Waiting for {config.WAIT_TIME_AFTER_CREATION} seconds for the bonding curve to stabilize..." + f"Waiting for {self.wait_time_after_creation} seconds for the bonding curve to stabilize..." ) - await asyncio.sleep(config.WAIT_TIME_AFTER_CREATION) + await asyncio.sleep(self.wait_time_after_creation) + # Buy token logger.info( f"Buying {self.buy_amount:.6f} SOL worth of {token_info.symbol}..." ) buy_result: TradeResult = await self.buyer.execute(token_info) if buy_result.success: - logger.info(f"Successfully bought {token_info.symbol}") - self._log_trade( - "buy", - token_info, - buy_result.price, # type: ignore - buy_result.amount, # type: ignore - buy_result.tx_signature, - ) - self.traded_mints.add(token_info.mint) + await self._handle_successful_buy(token_info, buy_result) else: - logger.error( - f"Failed to buy {token_info.symbol}: {buy_result.error_message}" - ) - # Close ATA if enabled - await handle_cleanup_after_failure(self.solana_client, self.wallet, token_info.mint, self.priority_fee_manager) + await self._handle_failed_buy(token_info, buy_result) - # Sell token if not in marry mode - if not marry_mode and buy_result.success: + # Only wait for next token in yolo mode + if self.yolo_mode: logger.info( - f"Waiting for {config.WAIT_TIME_AFTER_BUY} seconds before selling..." + f"YOLO mode enabled. Waiting {self.wait_time_before_new_token} seconds before looking for next token..." ) - await asyncio.sleep(config.WAIT_TIME_AFTER_BUY) - - logger.info(f"Selling {token_info.symbol}...") - sell_result: TradeResult = await self.seller.execute(token_info) - - if sell_result.success: - logger.info(f"Successfully sold {token_info.symbol}") - self._log_trade( - "sell", - token_info, - sell_result.price, # type: ignore - sell_result.amount, # type: ignore - sell_result.tx_signature, - ) - # Close ATA if enabled - await handle_cleanup_after_sell(self.solana_client, self.wallet, token_info.mint, self.priority_fee_manager) - else: - logger.error( - f"Failed to sell {token_info.symbol}: {sell_result.error_message}" - ) - elif marry_mode: - logger.info("Marry mode enabled. Skipping sell operation.") - - # Wait before looking for the next token - if yolo_mode: - logger.info( - f"YOLO mode enabled. Waiting {config.WAIT_TIME_BEFORE_NEW_TOKEN} seconds before looking for next token..." - ) - await asyncio.sleep(config.WAIT_TIME_BEFORE_NEW_TOKEN) + await asyncio.sleep(self.wait_time_before_new_token) except Exception as e: logger.error(f"Error handling token {token_info.symbol}: {e!s}") - async def _save_token_info(self, token_info: TokenInfo) -> None: + async def _handle_successful_buy( + self, token_info: TokenInfo, buy_result: TradeResult + ) -> None: + """Handle successful token purchase. + + Args: + token_info: Token information + buy_result: The result of the buy operation + """ + logger.info(f"Successfully bought {token_info.symbol}") + self._log_trade( + "buy", + token_info, + buy_result.price, # type: ignore + buy_result.amount, # type: ignore + buy_result.tx_signature, + ) + self.traded_mints.add(token_info.mint) + + # Sell token if not in marry mode + if not self.marry_mode: + logger.info( + f"Waiting for {self.wait_time_after_buy} seconds before selling..." + ) + await asyncio.sleep(self.wait_time_after_buy) + + logger.info(f"Selling {token_info.symbol}...") + sell_result: TradeResult = await self.seller.execute(token_info) + + if sell_result.success: + logger.info(f"Successfully sold {token_info.symbol}") + self._log_trade( + "sell", + token_info, + sell_result.price, # type: ignore + sell_result.amount, # type: ignore + sell_result.tx_signature, + ) + # Close ATA if enabled + await handle_cleanup_after_sell( + self.solana_client, + self.wallet, + token_info.mint, + self.priority_fee_manager, + self.cleanup_mode, + self.cleanup_with_priority_fee, + self.cleanup_force_close_with_burn + ) + else: + logger.error( + f"Failed to sell {token_info.symbol}: {sell_result.error_message}" + ) + else: + logger.info("Marry mode enabled. Skipping sell operation.") + + async def _handle_failed_buy( + self, token_info: TokenInfo, buy_result: TradeResult + ) -> None: + """Handle failed token purchase. + + Args: + token_info: Token information + buy_result: The result of the buy operation + """ + logger.error( + f"Failed to buy {token_info.symbol}: {buy_result.error_message}" + ) + # Close ATA if enabled + await handle_cleanup_after_failure( + self.solana_client, + self.wallet, + token_info.mint, + self.priority_fee_manager, + self.cleanup_mode, + self.cleanup_with_priority_fee, + self.cleanup_force_close_with_burn + ) + + async def _save_token_info( + self, token_info: TokenInfo + ) -> None: """Save token information to a file. Args: token_info: Token information """ - os.makedirs("trades", exist_ok=True) - file_name = os.path.join("trades", f"{token_info.mint}.txt") + try: + os.makedirs("trades", exist_ok=True) + file_name = os.path.join("trades", f"{token_info.mint}.txt") - with open(file_name, "w") as file: - file.write(json.dumps(token_info.to_dict(), indent=2)) + with open(file_name, "w") as file: + file.write(json.dumps(token_info.to_dict(), indent=2)) - logger.info(f"Token information saved to {file_name}") + logger.info(f"Token information saved to {file_name}") + except Exception as e: + logger.error(f"Failed to save token information: {e!s}") def _log_trade( self, @@ -327,17 +523,20 @@ class PumpTrader: amount: Trade amount in SOL tx_hash: Transaction hash """ - os.makedirs("trades", exist_ok=True) + try: + os.makedirs("trades", exist_ok=True) - log_entry = { - "timestamp": datetime.utcnow().isoformat(), - "action": action, - "token_address": str(token_info.mint), - "symbol": token_info.symbol, - "price": price, - "amount": amount, - "tx_hash": str(tx_hash), - } + log_entry = { + "timestamp": datetime.utcnow().isoformat(), + "action": action, + "token_address": str(token_info.mint), + "symbol": token_info.symbol, + "price": price, + "amount": amount, + "tx_hash": str(tx_hash) if tx_hash else None, + } - with open("trades/trades.log", "a") as log_file: - log_file.write(json.dumps(log_entry) + "\n") + with open("trades/trades.log", "a") as log_file: + log_file.write(json.dumps(log_entry) + "\n") + except Exception as e: + logger.error(f"Failed to log trade information: {e!s}") \ No newline at end of file diff --git a/src/utils/logger.py b/src/utils/logger.py index 9fa07f5..0c0cf90 100644 --- a/src/utils/logger.py +++ b/src/utils/logger.py @@ -3,7 +3,6 @@ Logging utilities for the pump.fun trading bot. """ import logging -import sys # Global dict to store loggers _loggers: dict[str, logging.Logger] = {} @@ -27,16 +26,6 @@ def get_logger(name: str, level: int = logging.INFO) -> logging.Logger: logger = logging.getLogger(name) logger.setLevel(level) - if not logger.handlers: - formatter = logging.Formatter( - "%(asctime)s - %(name)s - %(levelname)s - %(message)s", - datefmt="%Y-%m-%d %H:%M:%S", - ) - - console_handler = logging.StreamHandler(sys.stdout) - console_handler.setFormatter(formatter) - logger.addHandler(console_handler) - _loggers[name] = logger return logger @@ -52,6 +41,11 @@ def setup_file_logging( """ root_logger = logging.getLogger() + # Check if file handler with same filename already exists + for handler in root_logger.handlers: + if isinstance(handler, logging.FileHandler) and handler.baseFilename == filename: + return # File handler already added + formatter = logging.Formatter( "%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S", diff --git a/trades/LR7TYDeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump.txt b/trades/LR7TYDeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump.txt deleted file mode 100644 index 987a833..0000000 --- a/trades/LR7TYDeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump.txt +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "RAKAMAKAFO", - "symbol": "MAKAFO", - "uri": "https://cf-ipfs.com/ipfs/QmYyNfffJL8aLzsQQBxSDEmbQ2STSPS35rfFwE7isjJiVu", - "mint": "LR7TYDeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump", - "bondingCurve": "9UznPeXvYtPqq8saSyM65cC8LCtyAKPnLCGG7Mq1pUTq", - "associatedBondingCurve": "CTe6pwwgzEUvRmPGRE2EZXrESpiWawK5TwyJhgvFgJ6T", - "user": "Ev5WEEJ2Y1qKGE8KCNvBNVEgy9fJUuttA93pMo2fcGFd" -} \ No newline at end of file diff --git a/trades/trades.log b/trades/trades.log index d44d1ed..d226794 100644 --- a/trades/trades.log +++ b/trades/trades.log @@ -1,12 +1,2 @@ -{"timestamp": "2024-08-22T10:51:30.306580", "action": "buy", "token_address": "LR7TYDeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump", "price": 1.668947042812354e-07, "tx_hash": "XZDRuEnaBNEeuFAiGyJsqBmVF3uZvFqAQUKsZVKTxsThHxN43LiRqy3T4cSH4dRXGB7c5A4855iYQDLY4BNJJhP"} -{"timestamp": "2024-08-22T10:51:56.116951", "action": "sell", "token_address": "LR7TYDeWamU7MsCXFH7ydwJYaZfRJs8pzgCQrh2pump", "price": 1.668947042812354e-07, "tx_hash": "WQHnEoyztfDf5FJyQRXDGxxtqf2EcMGs8SET43KuYU8Dpdunruku22SHnfiH2xsUi1hTBjC1msvTcnU78fFKrc2"} -{"timestamp": "2025-03-06T21:53:16.417950", "action": "buy", "token_address": "7ztobvXwBd8UBn2mUuatjpmEmn7EZpifXv98xjCrpump", "symbol": "$Auti", "price": 3.3860819583417394e-08, "amount": 29.532657871333047, "tx_hash": "4fUuh16912cK7xiZsvY1scu5P5NtWGwowcAYXQvvr1FWAvPD57mPKfrq5PMMzMULhMiVn86fiDCUfwWt6B7NPNxV"} -{"timestamp": "2025-03-06T21:53:33.704455", "action": "sell", "token_address": "7ztobvXwBd8UBn2mUuatjpmEmn7EZpifXv98xjCrpump", "symbol": "$Auti", "price": 3.38403327639918e-08, "amount": 29.532657, "tx_hash": "4TRFmKYkibrgn2fX4UT7mM8i7Lpu3FgXKvY1d6AsEUaxLj11JxWo61xxqYGW7B6wzdaY9LWVWZnTSDTZUUNs5pFU"} -{"timestamp": "2025-03-06T22:04:01.067407", "action": "buy", "token_address": "5jch2xH14RBWZgKwmxEUJwC4howFNBCBqbnZ5uguGujj", "symbol": "$MOONWEN", "price": 2.8543914418196173e-08, "amount": 35.03373732659877, "tx_hash": "VCWbh79TmNWtt9kjex5LLtWKGHjc3nPBQVJdynC7B85nbnXx4DBgwguqAe6StYVAUS7jzP17pN5FWWGvbiUBRfk"} -{"timestamp": "2025-03-06T22:04:48.843998", "action": "sell", "token_address": "5jch2xH14RBWZgKwmxEUJwC4howFNBCBqbnZ5uguGujj", "symbol": "$MOONWEN", "price": 2.8056120698010214e-08, "amount": 35.033737, "tx_hash": "RLx8hFVd5DVZdUg5vN41Fs8Yz66yTEUEvegasuHwYQ7DTJhPcCdggoNBtipb93VPVdtQLZydxc9ZKRZ1uzK6KzH"} -{"timestamp": "2025-03-06T22:05:11.444317", "action": "buy", "token_address": "C9GrXnLPkD4fdj83ua7TFndtxvCVxdYyZzqrMBBZpump", "symbol": "CRYPTO ", "price": 3.383038210726932e-08, "amount": 29.559228649242023, "tx_hash": "3E4RT5kvRezWJmQKJ9wmcAiDcYZefohxFVNdEZPweE9vhq215rSmqBn4Dv4ypH5KUUuNJzKdhjyrGU2b47woWr9e"} -{"timestamp": "2025-03-06T22:05:25.789876", "action": "sell", "token_address": "C9GrXnLPkD4fdj83ua7TFndtxvCVxdYyZzqrMBBZpump", "symbol": "CRYPTO ", "price": 3.3830384158620706e-08, "amount": 29.559228, "tx_hash": "3oyJVzsRvnQV5qBsEp4hSk97eFWzUjzfWWa16GMUXtMd7JBmbQkTkmvs7N8nagYcJvqfFhzhCKZEG6onxjYYQbwX"} -{"timestamp": "2025-04-03T21:55:46.849123", "action": "buy", "token_address": "4rHSLfYLkFmHTcyCF8thnUmyuX4MBrHdjdftZXM7pump", "symbol": "GCT", "price": 3.3333333333333334e-08, "amount": 30, "tx_hash": "35UncvxypdvuKC4gD3jXnmmtwfBfEhFc2Y2JLfszzvHqsZumcjp2CMkXH4HD7iexgkPEpG1je73bwnFWbNQjxNUS"} -{"timestamp": "2025-04-03T21:56:05.816803", "action": "sell", "token_address": "4rHSLfYLkFmHTcyCF8thnUmyuX4MBrHdjdftZXM7pump", "symbol": "GCT", "price": 3.1716731316027684e-08, "amount": 30.0, "tx_hash": "2NoMsScE5tJu3gzvrSn1uarnVZ4DcgXVLsc7BEwJ1ndU7T69xPbgyk53YW4MVCEfrSvdoaCup6VyM9pnf6YR6pGc"} -{"timestamp": "2025-04-03T21:56:49.686823", "action": "buy", "token_address": "Hx4wR3Z7tEjfVip8jHFFsF3d4LwNmXgjBRkNR9qSUarF", "symbol": "Pablo", "price": 5.067234074416521e-08, "amount": 19.734632055953472, "tx_hash": "52hQYXPjtP1Rr945UW5peef6qbxThUubyBbEbcnGCCJ146nxx6bZXj3mCpWtpqpmMe82oNkEFwMms26JqhAaJMXL"} -{"timestamp": "2025-04-03T21:57:09.656288", "action": "sell", "token_address": "Hx4wR3Z7tEjfVip8jHFFsF3d4LwNmXgjBRkNR9qSUarF", "symbol": "Pablo", "price": 2.8113188162567704e-08, "amount": 19.734632, "tx_hash": "xzpCFN8PgtPvXyRfQjkhHkPQcdzTNb6JXtCsQixtsMQabiBt9yNMSb7JLxRCT295rbVZuuocRNxYe6AqAiRnE91"} +{"timestamp": "2025-04-24T20:30:13.087092", "action": "buy", "token_address": "DWMUmRQUZPCBA1gwdDxTJuz6JHnQkREiWMyQpsKWGp9v", "symbol": "U8", "price": 5e-06, "amount": 20, "tx_hash": "3JvdfCep45PUB6rCcH4dB2NuwvFP8n67SCUxqJMt4MuN5ekHYc6J27aCUfwNUK3hh5rSyKNYAWXya5vQAT2qQivB"} +{"timestamp": "2025-04-24T20:30:32.759177", "action": "sell", "token_address": "DWMUmRQUZPCBA1gwdDxTJuz6JHnQkREiWMyQpsKWGp9v", "symbol": "U8", "price": 3.805530050663904e-08, "amount": 20.0, "tx_hash": "5cveLfU7XhPNCPMCZfTXyugJpmAQNmi7zr81PSqs8DsP1T2swYFjJwaB5hNSf3kFPfRzgzd7QZBVaZLd5MqsJevB"}