wip(core): platform aware trading

This commit is contained in:
smypmsa
2025-08-02 13:27:07 +00:00
parent 9ec9eeb793
commit 227ef0b3bd
9 changed files with 699 additions and 506 deletions
+137 -99
View File
@@ -12,18 +12,14 @@ from config_loader import (
get_platform_from_config,
load_bot_config,
print_config_summary,
validate_platform_listener_combination,
)
from trading.universal_trader import UniversalTrader
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
"""
"""Set up logging to file for a specific bot instance."""
log_dir = Path("logs")
log_dir.mkdir(exist_ok=True)
@@ -34,96 +30,119 @@ def setup_logging(bot_name: str):
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
"""
"""Start a trading bot with the configuration from the specified path."""
cfg = load_bot_config(config_path)
setup_logging(cfg["name"])
print_config_summary(cfg)
# Get platform from configuration
platform = get_platform_from_config(cfg)
# Initialize universal trader with platform-specific configuration
trader = UniversalTrader(
# Connection settings
rpc_endpoint=cfg["rpc_endpoint"],
wss_endpoint=cfg["wss_endpoint"],
private_key=cfg["private_key"],
# Platform configuration
platform=platform,
# 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),
# Exit strategy configuration
exit_strategy=cfg["trade"].get("exit_strategy", "time_based"),
take_profit_percentage=cfg["trade"].get("take_profit_percentage"),
stop_loss_percentage=cfg["trade"].get("stop_loss_percentage"),
max_hold_time=cfg["trade"].get("max_hold_time"),
price_check_interval=cfg["trade"].get("price_check_interval", 10),
# 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"),
geyser_auth_type=cfg.get("geyser", {}).get("auth_type", "x-token"),
# PumpPortal configuration (if applicable)
pumpportal_url=cfg.get("pumpportal", {}).get(
"url", "wss://pumpportal.fun/api/data"
),
# 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("filters", {}).get("max_token_age", 0.001),
token_wait_timeout=cfg.get("timing", {}).get("token_wait_timeout", 120),
# 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),
)
# Get and validate platform from configuration
try:
platform = get_platform_from_config(cfg)
logging.info(f"Detected platform: {platform.value}")
except ValueError as e:
logging.exception(f"Platform configuration error: {e}")
return
await trader.start()
# Validate platform support
try:
from platforms import platform_factory
if not platform_factory.registry.is_platform_supported(platform):
logging.error(f"Platform {platform.value} is not supported. Available platforms: {[p.value for p in platform_factory.get_supported_platforms()]}")
return
except Exception as e:
logging.exception(f"Could not validate platform support: {e}")
return
# Validate listener compatibility
listener_type = cfg["filters"]["listener_type"]
if not validate_platform_listener_combination(platform, listener_type):
from config_loader import get_supported_listeners_for_platform
supported = get_supported_listeners_for_platform(platform)
logging.error(f"Listener '{listener_type}' is not compatible with platform '{platform.value}'. Supported listeners: {supported}")
return
# Initialize universal trader with platform-specific configuration
try:
trader = UniversalTrader(
# Connection settings
rpc_endpoint=cfg["rpc_endpoint"],
wss_endpoint=cfg["wss_endpoint"],
private_key=cfg["private_key"],
# Platform configuration - pass platform enum directly
platform=platform,
# 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),
# Exit strategy configuration
exit_strategy=cfg["trade"].get("exit_strategy", "time_based"),
take_profit_percentage=cfg["trade"].get("take_profit_percentage"),
stop_loss_percentage=cfg["trade"].get("stop_loss_percentage"),
max_hold_time=cfg["trade"].get("max_hold_time"),
price_check_interval=cfg["trade"].get("price_check_interval", 10),
# 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"),
geyser_auth_type=cfg.get("geyser", {}).get("auth_type", "x-token"),
# PumpPortal configuration (if applicable)
pumpportal_url=cfg.get("pumpportal", {}).get(
"url", "wss://pumpportal.fun/api/data"
),
# 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("filters", {}).get("max_token_age", 0.001),
token_wait_timeout=cfg.get("timing", {}).get("token_wait_timeout", 120),
# 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()
except Exception as e:
logging.exception(f"Failed to initialize or start trader: {e}")
raise
def run_bot_process(config_path):
@@ -131,11 +150,7 @@ def run_bot_process(config_path):
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.
"""
"""Run all bots defined in YAML files in the 'bots' directory."""
bot_dir = Path("bots")
if not bot_dir.exists():
logging.error(f"Bot directory '{bot_dir}' not found")
@@ -162,19 +177,32 @@ def run_all_bots():
skipped_bots += 1
continue
# Validate platform support
# Validate platform configuration
try:
platform = get_platform_from_config(cfg)
# Check platform support
from platforms import platform_factory
if not platform_factory.registry.is_platform_supported(platform):
logging.error(f"Platform {platform.value} is not supported for bot '{bot_name}'. Skipping...")
logging.error(f"Platform {platform.value} is not supported for bot '{bot_name}'. Available platforms: {[p.value for p in platform_factory.get_supported_platforms()]}")
skipped_bots += 1
continue
# Validate listener compatibility
listener_type = cfg["filters"]["listener_type"]
if not validate_platform_listener_combination(platform, listener_type):
from config_loader import get_supported_listeners_for_platform
supported = get_supported_listeners_for_platform(platform)
logging.error(f"Listener '{listener_type}' is not compatible with platform '{platform.value}' for bot '{bot_name}'. Supported listeners: {supported}")
skipped_bots += 1
continue
except Exception as e:
logging.exception(f"Invalid platform configuration for bot '{bot_name}': {e}. Skipping...")
skipped_bots += 1
continue
# Start bot in separate process or main process
if cfg.get("separate_process", False):
logging.info(f"Starting bot '{bot_name}' ({platform.value}) in separate process")
p = multiprocessing.Process(
@@ -185,13 +213,16 @@ def run_all_bots():
else:
logging.info(f"Starting bot '{bot_name}' ({platform.value}) in main process")
asyncio.run(start_bot(str(file)))
except Exception as e:
logging.exception(f"Failed to start bot from {file}: {e}")
skipped_bots += 1
logging.info(
f"Started {len(bot_files) - skipped_bots} bots, skipped {skipped_bots} disabled/invalid bots"
)
# Wait for all processes to complete
for p in processes:
p.join()
logging.info(f"Process {p.name} completed")
@@ -203,11 +234,18 @@ def main() -> None:
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)
# Log supported platforms
# Log supported platforms and listeners
try:
from platforms import platform_factory
supported_platforms = platform_factory.get_supported_platforms()
logging.info(f"Supported platforms: {[p.value for p in supported_platforms]}")
# Log listener compatibility for each platform
from config_loader import get_supported_listeners_for_platform
for platform in supported_platforms:
listeners = get_supported_listeners_for_platform(platform)
logging.info(f"Platform {platform.value} supports listeners: {listeners}")
except Exception as e:
logging.warning(f"Could not load platform information: {e}")
+174 -143
View File
@@ -1,8 +1,5 @@
"""
Updated configuration validation with platform support.
This extends the existing config_loader.py to support platform selection
and validation while maintaining backward compatibility.
Updated configuration validation with comprehensive platform support.
"""
import os
@@ -27,77 +24,33 @@ REQUIRED_FIELDS = [
]
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_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",
),
("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 (extended with platform support)
# Valid values for enum-like fields
VALID_VALUES = {
"filters.listener_type": ["logs", "blocks", "geyser", "pumpportal"],
"cleanup.mode": ["disabled", "on_fail", "after_sell", "post_session"],
"trade.exit_strategy": ["time_based", "tp_sl", "manual"],
"platform": ["pump_fun", "lets_bonk"], # Platform validation
"platform": ["pump_fun", "lets_bonk"],
}
# Platform-specific listener compatibility
PLATFORM_LISTENER_COMPATIBILITY = {
Platform.PUMP_FUN: ["logs", "blocks", "geyser", "pumpportal"],
Platform.LETS_BONK: ["logs", "blocks", "geyser"], # PumpPortal is pump.fun only
}
def load_bot_config(path: str) -> dict:
"""
Load and validate a bot configuration from a YAML file.
Extended to support platform selection with backward compatibility.
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
"""
"""Load and validate a bot configuration from a YAML file."""
with open(path) as f:
config = yaml.safe_load(f)
@@ -107,7 +60,6 @@ def load_bot_config(path: str) -> dict:
if os.path.exists(env_path):
load_dotenv(env_path, override=True)
else:
# If not found relative to config, try relative to current working directory
load_dotenv(env_file, override=True)
resolve_env_vars(config)
@@ -117,18 +69,11 @@ def load_bot_config(path: str) -> dict:
config["platform"] = "pump_fun"
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
"""
"""Recursively resolve environment variables in the configuration."""
def resolve_env(value):
if isinstance(value, str) and value.startswith("${") and value.endswith("}"):
env_var = value[2:-1]
@@ -149,19 +94,7 @@ def resolve_env_vars(config: dict) -> None:
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
"""
"""Get a nested value from the configuration using dot notation."""
keys = path.split(".")
value = config
for key in keys:
@@ -172,16 +105,7 @@ def get_nested_value(config: dict, path: str) -> Any:
def validate_config(config: dict) -> None:
"""
Validate the configuration against defined rules.
Extended to include platform-specific validation.
Args:
config: Configuration dictionary to validate
Raises:
ValueError: If the configuration is invalid
"""
"""Validate the configuration against defined rules with platform support."""
# Validate required fields
for field in REQUIRED_FIELDS:
get_nested_value(config, field)
@@ -198,10 +122,8 @@ def validate_config(config: dict) -> None:
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
@@ -211,7 +133,6 @@ def validate_config(config: dict) -> None:
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
@@ -219,11 +140,8 @@ def validate_config(config: dict) -> None:
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"
)
raise ValueError("Cannot enable both dynamic and fixed priority fees simultaneously")
except ValueError:
# Skip if one of the fields is missing
pass
# Platform-specific validation
@@ -238,16 +156,30 @@ def validate_config(config: dict) -> None:
def validate_platform_config(config: dict, platform: Platform) -> None:
"""
Validate platform-specific configuration requirements.
"""Validate platform-specific configuration requirements."""
# Check if platform is supported
try:
from platforms import platform_factory
if not platform_factory.registry.is_platform_supported(platform):
raise ValueError(f"Platform {platform.value} is not supported. Available platforms: {[p.value for p in platform_factory.get_supported_platforms()]}")
except ImportError:
# If platform factory not available, just validate enum
pass
Args:
config: Configuration dictionary
platform: Platform enum value
# Validate listener compatibility with platform
try:
listener_type = get_nested_value(config, "filters.listener_type")
compatible_listeners = PLATFORM_LISTENER_COMPATIBILITY.get(platform, [])
if listener_type not in compatible_listeners:
raise ValueError(
f"Listener type '{listener_type}' is not compatible with platform '{platform.value}'. "
f"Compatible listeners: {compatible_listeners}"
)
except ValueError:
pass
Raises:
ValueError: If platform-specific config is invalid
"""
# Platform-specific configuration validation
if platform == Platform.PUMP_FUN:
# pump.fun doesn't require additional config beyond base requirements
pass
@@ -259,18 +191,7 @@ def validate_platform_config(config: dict, platform: Platform) -> None:
def get_platform_from_config(config: dict) -> Platform:
"""
Extract platform enum from configuration.
Args:
config: Configuration dictionary
Returns:
Platform enum value
Raises:
ValueError: If platform is invalid
"""
"""Extract platform enum from configuration."""
platform_str = config.get("platform", "pump_fun")
try:
return Platform(platform_str)
@@ -278,46 +199,156 @@ def get_platform_from_config(config: dict) -> Platform:
raise ValueError(f"Invalid platform '{platform_str}'. Must be one of: {[p.value for p in Platform]}")
def print_config_summary(config: dict) -> None:
"""
Print a summary of the loaded configuration.
Extended to show platform information.
def validate_platform_listener_combination(platform: Platform, listener_type: str) -> bool:
"""Check if a platform and listener type are compatible.
Args:
config: Configuration dictionary
platform: Platform enum
listener_type: Listener type string
Returns:
True if combination is valid
"""
compatible_listeners = PLATFORM_LISTENER_COMPATIBILITY.get(platform, [])
return listener_type in compatible_listeners
def get_supported_listeners_for_platform(platform: Platform) -> list[str]:
"""Get list of supported listener types for a platform.
Args:
platform: Platform enum
Returns:
List of supported listener types
"""
return PLATFORM_LISTENER_COMPATIBILITY.get(platform, [])
def get_platform_specific_required_config(platform: Platform) -> list[str]:
"""Get platform-specific required configuration paths.
Args:
platform: Platform enum
Returns:
List of additional required config paths for the platform
"""
if platform == Platform.PUMP_FUN:
return [] # No additional requirements
elif platform == Platform.LETS_BONK:
return [] # No additional requirements yet
else:
return []
def print_config_summary(config: dict) -> None:
"""Print a summary of the loaded configuration with platform info."""
platform_str = config.get("platform", "pump_fun")
print(f"Bot name: {config.get('name', 'unnamed')}")
print(f"Platform: {platform_str}")
print(
f"Listener type: {config.get('filters', {}).get('listener_type', 'not configured')}"
)
print(f"Listener type: {config.get('filters', {}).get('listener_type', 'not configured')}")
# Validate platform-listener combination
try:
platform = Platform(platform_str)
listener_type = config.get('filters', {}).get('listener_type')
if listener_type and not validate_platform_listener_combination(platform, listener_type):
print(f"WARNING: Listener '{listener_type}' may not be compatible with platform '{platform_str}'")
except ValueError:
print(f"WARNING: Invalid platform '{platform_str}'")
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'}"
)
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(f" - Fixed fee: {fees.get('fixed_amount', 'not configured')} microlamports")
print("Configuration loaded successfully!")
if __name__ == "__main__":
# Example usage with platform configuration
config = load_bot_config("bots/bot-sniper.yaml")
print_config_summary(config)
def validate_all_platform_configs(config_dir: str = "bots") -> dict[str, Any]:
"""Validate all bot configurations in a directory.
platform = get_platform_from_config(config)
print(f"Detected platform: {platform}")
Args:
config_dir: Directory containing bot config files
Returns:
Dictionary with validation results
"""
import glob
import os
results = {
"valid_configs": [],
"invalid_configs": [],
"platform_distribution": {},
"listener_distribution": {},
}
config_files = glob.glob(os.path.join(config_dir, "*.yaml"))
for config_file in config_files:
try:
config = load_bot_config(config_file)
platform = get_platform_from_config(config)
listener_type = config.get('filters', {}).get('listener_type', 'unknown')
results["valid_configs"].append({
"file": config_file,
"name": config.get("name"),
"platform": platform.value,
"listener": listener_type,
"enabled": config.get("enabled", True)
})
# Track distributions
platform_key = platform.value
results["platform_distribution"][platform_key] = results["platform_distribution"].get(platform_key, 0) + 1
results["listener_distribution"][listener_type] = results["listener_distribution"].get(listener_type, 0) + 1
except Exception as e:
results["invalid_configs"].append({
"file": config_file,
"error": str(e)
})
return results
if __name__ == "__main__":
# Example usage with platform configuration validation
import sys
if len(sys.argv) > 1:
config_path = sys.argv[1]
try:
config = load_bot_config(config_path)
print_config_summary(config)
platform = get_platform_from_config(config)
print(f"Detected platform: {platform}")
print(f"Supported listeners for this platform: {get_supported_listeners_for_platform(platform)}")
except Exception as e:
print(f"Configuration error: {e}")
else:
# Validate all configs in bots directory
results = validate_all_platform_configs()
print("Configuration validation results:")
print(f"Valid configs: {len(results['valid_configs'])}")
print(f"Invalid configs: {len(results['invalid_configs'])}")
print(f"Platform distribution: {results['platform_distribution']}")
print(f"Listener distribution: {results['listener_distribution']}")
if results['invalid_configs']:
print("\nInvalid configurations:")
for invalid in results['invalid_configs']:
print(f" {invalid['file']}: {invalid['error']}")
+165
View File
@@ -0,0 +1,165 @@
"""
Pump.fun bonding curve manager for price calculations and curve state management.
"""
import struct
from dataclasses import dataclass
from solders.pubkey import Pubkey
from core.client import SolanaClient
from core.pubkeys import LAMPORTS_PER_SOL, TOKEN_DECIMALS
from utils.logger import get_logger
logger = get_logger(__name__)
# Bonding curve discriminator
CURVE_DISCRIMINATOR = bytes([23, 203, 71, 8, 209, 70, 227, 3])
@dataclass
class BondingCurveState:
"""Represents the state of a pump.fun bonding curve."""
virtual_token_reserves: int
virtual_sol_reserves: int
real_token_reserves: int
real_sol_reserves: int
token_total_supply: int
complete: bool
creator: Pubkey
@property
def token_reserves(self) -> float:
"""Token reserves in decimal form."""
return self.virtual_token_reserves / 10**TOKEN_DECIMALS
@property
def sol_reserves(self) -> float:
"""SOL reserves in decimal form."""
return self.virtual_sol_reserves / LAMPORTS_PER_SOL
def calculate_price(self) -> float:
"""Calculate current token price in SOL."""
if self.virtual_token_reserves <= 0:
return 0.0
# Price = sol_reserves / token_reserves
price_lamports = self.virtual_sol_reserves / self.virtual_token_reserves
return price_lamports * (10**TOKEN_DECIMALS) / LAMPORTS_PER_SOL
class BondingCurveManager:
"""Manages pump.fun bonding curve operations."""
def __init__(self, client: SolanaClient):
"""Initialize bonding curve manager.
Args:
client: Solana RPC client
"""
self.client = client
async def get_curve_state(self, curve_address: Pubkey) -> BondingCurveState:
"""Get the current state of a bonding curve.
Args:
curve_address: Address of the bonding curve
Returns:
BondingCurveState with current curve data
Raises:
ValueError: If curve data is invalid or inaccessible
"""
try:
account = await self.client.get_account_info(curve_address)
if not account.data:
raise ValueError(f"No data in bonding curve account {curve_address}")
curve_state = self._decode_curve_state(account.data)
return curve_state
except Exception as e:
logger.error(f"Failed to get curve state: {e!s}")
raise ValueError(f"Invalid bonding curve state: {e!s}")
async def calculate_price(self, curve_address: Pubkey) -> float:
"""Calculate current token price from bonding curve.
Args:
curve_address: Address of the bonding curve
Returns:
Current token price in SOL
"""
curve_state = await self.get_curve_state(curve_address)
return curve_state.calculate_price()
def _decode_curve_state(self, data: bytes) -> BondingCurveState:
"""Decode bonding curve state from raw account data.
Args:
data: Raw account data
Returns:
Decoded BondingCurveState
Raises:
ValueError: If data format is invalid
"""
if len(data) < 8:
raise ValueError("Curve data too short")
# Check discriminator
if not data.startswith(CURVE_DISCRIMINATOR):
raise ValueError("Invalid curve discriminator")
offset = 8
try:
# Decode based on pump.fun BondingCurve structure:
# - virtual_token_reserves: u64 (8 bytes)
# - virtual_sol_reserves: u64 (8 bytes)
# - real_token_reserves: u64 (8 bytes)
# - real_sol_reserves: u64 (8 bytes)
# - token_total_supply: u64 (8 bytes)
# - complete: bool (1 byte)
# - padding: 7 bytes
# - creator: Pubkey (32 bytes)
virtual_token_reserves = struct.unpack_from("<Q", data, offset)[0]
offset += 8
virtual_sol_reserves = struct.unpack_from("<Q", data, offset)[0]
offset += 8
real_token_reserves = struct.unpack_from("<Q", data, offset)[0]
offset += 8
real_sol_reserves = struct.unpack_from("<Q", data, offset)[0]
offset += 8
token_total_supply = struct.unpack_from("<Q", data, offset)[0]
offset += 8
complete = bool(struct.unpack_from("<B", data, offset)[0])
offset += 1
# Skip padding
offset += 7
creator = Pubkey.from_bytes(data[offset:offset + 32])
return BondingCurveState(
virtual_token_reserves=virtual_token_reserves,
virtual_sol_reserves=virtual_sol_reserves,
real_token_reserves=real_token_reserves,
real_sol_reserves=real_sol_reserves,
token_total_supply=token_total_supply,
complete=complete,
creator=creator,
)
except Exception as e:
raise ValueError(f"Failed to decode curve state: {e}")
+19 -33
View File
@@ -6,12 +6,10 @@ import asyncio
from collections.abc import Awaitable, Callable
import grpc
from solders.pubkey import Pubkey
from geyser.generated import geyser_pb2, geyser_pb2_grpc
from interfaces.core import Platform, TokenInfo
from monitoring.base_listener import BaseTokenListener
from platforms import get_platform_implementations
from utils.logger import get_logger
logger = get_logger(__name__)
@@ -27,14 +25,7 @@ class UniversalGeyserListener(BaseTokenListener):
geyser_auth_type: str,
platforms: list[Platform] | None = None,
):
"""Initialize universal Geyser listener.
Args:
geyser_endpoint: Geyser gRPC endpoint URL
geyser_api_token: API token for authentication
geyser_auth_type: authentication type ('x-token' or 'basic')
platforms: List of platforms to monitor (if None, monitor all supported platforms)
"""
"""Initialize universal Geyser listener."""
super().__init__()
self.geyser_endpoint = geyser_endpoint
self.geyser_api_token = geyser_api_token
@@ -51,7 +42,6 @@ class UniversalGeyserListener(BaseTokenListener):
from platforms import platform_factory
if platforms is None:
# Monitor all supported platforms
self.platforms = platform_factory.get_supported_platforms()
else:
self.platforms = platforms
@@ -60,13 +50,13 @@ class UniversalGeyserListener(BaseTokenListener):
self.platform_parsers = {}
self.platform_program_ids = set()
# Create a temporary client for getting parsers
from core.client import SolanaClient
temp_client = SolanaClient("http://temp")
for platform in self.platforms:
try:
# We'll need a dummy client for getting the parser - this is a design issue we should fix
from core.client import SolanaClient
dummy_client = SolanaClient("http://localhost") # Won't be used for parsing
implementations = get_platform_implementations(platform, dummy_client)
implementations = platform_factory.create_for_platform(platform, temp_client)
parser = implementations.event_parser
self.platform_parsers[platform] = parser
self.platform_program_ids.add(parser.get_program_id())
@@ -78,6 +68,7 @@ class UniversalGeyserListener(BaseTokenListener):
async def _create_geyser_connection(self):
"""Establish a secure connection to the Geyser endpoint."""
if self.auth_type == "x-token":
auth = grpc.metadata_call_credentials(
lambda _, callback: callback(
@@ -92,10 +83,12 @@ class UniversalGeyserListener(BaseTokenListener):
)
creds = grpc.composite_channel_credentials(grpc.ssl_channel_credentials(), auth)
channel = grpc.aio.secure_channel(self.geyser_endpoint, creds)
return geyser_pb2_grpc.GeyserStub(channel), channel
def _create_subscription_request(self):
"""Create a subscription request for all monitored platforms."""
request = geyser_pb2.SubscribeRequest()
# Add all platform program IDs to the filter
@@ -113,13 +106,7 @@ class UniversalGeyserListener(BaseTokenListener):
match_string: str | None = None,
creator_address: str | None = None,
) -> None:
"""Listen for new token creations using Geyser subscription.
Args:
token_callback: Callback function for new tokens
match_string: Optional string to match in token name/symbol
creator_address: Optional creator address to filter by
"""
"""Listen for new token creations using Geyser subscription."""
if not self.platform_parsers:
logger.error("No platform parsers available. Cannot listen for tokens.")
return
@@ -161,8 +148,12 @@ class UniversalGeyserListener(BaseTokenListener):
await token_callback(token_info)
except grpc.aio.AioRpcError as e:
logger.error(f"gRPC error: {e.details()}")
except Exception as e:
import grpc
if isinstance(e, grpc.aio.AioRpcError):
logger.error(f"gRPC error: {e.details()}")
else:
logger.error(f"Geyser error: {e}")
await asyncio.sleep(5)
finally:
@@ -174,14 +165,7 @@ class UniversalGeyserListener(BaseTokenListener):
await asyncio.sleep(10)
async def _process_update(self, update) -> TokenInfo | None:
"""Process a Geyser update and extract token creation info.
Args:
update: Geyser update from the subscription
Returns:
TokenInfo if a token creation is found, None otherwise
"""
"""Process a Geyser update and extract token creation info."""
try:
if not update.HasField("transaction"):
return None
@@ -191,6 +175,8 @@ class UniversalGeyserListener(BaseTokenListener):
if msg is None:
return None
from solders.pubkey import Pubkey
for ix in msg.instructions:
# Check which platform this instruction belongs to
program_idx = ix.program_id_index
+9 -23
View File
@@ -1,7 +1,6 @@
"""
Universal logs listener that works with any platform through the interface system.
"""
import asyncio
import json
from collections.abc import Awaitable, Callable
@@ -10,7 +9,6 @@ import websockets
from interfaces.core import Platform, TokenInfo
from monitoring.base_listener import BaseTokenListener
from platforms import get_platform_implementations
from utils.logger import get_logger
logger = get_logger(__name__)
@@ -47,13 +45,13 @@ class UniversalLogsListener(BaseTokenListener):
self.platform_parsers = {}
self.platform_program_ids = []
# Create a temporary client for getting parsers (stateless parsers don't use it)
from core.client import SolanaClient
temp_client = SolanaClient("http://temp")
for platform in self.platforms:
try:
# We'll need a dummy client for getting the parser
from core.client import SolanaClient
dummy_client = SolanaClient("http://localhost") # Won't be used for parsing
implementations = get_platform_implementations(platform, dummy_client)
implementations = platform_factory.create_for_platform(platform, temp_client)
parser = implementations.event_parser
self.platform_parsers[platform] = parser
self.platform_program_ids.append(str(parser.get_program_id()))
@@ -130,11 +128,11 @@ class UniversalLogsListener(BaseTokenListener):
websocket: Active WebSocket connection
"""
# Subscribe to logs for all monitored platforms
for program_id in self.platform_program_ids:
for i, program_id in enumerate(self.platform_program_ids):
subscription_message = json.dumps(
{
"jsonrpc": "2.0",
"id": len(self.platform_program_ids), # Use different IDs
"id": i + 1,
"method": "logsSubscribe",
"params": [
{"mentions": [program_id]},
@@ -155,11 +153,7 @@ class UniversalLogsListener(BaseTokenListener):
logger.warning(f"Unexpected subscription response: {response}")
async def _ping_loop(self, websocket) -> None:
"""Keep connection alive with pings.
Args:
websocket: Active WebSocket connection
"""
"""Keep connection alive with pings."""
try:
while True:
await asyncio.sleep(self.ping_interval)
@@ -168,7 +162,6 @@ class UniversalLogsListener(BaseTokenListener):
await asyncio.wait_for(pong_waiter, timeout=10)
except TimeoutError:
logger.warning("Ping timeout - server not responding")
# Force reconnection
await websocket.close()
return
except asyncio.CancelledError:
@@ -177,14 +170,7 @@ class UniversalLogsListener(BaseTokenListener):
logger.error(f"Ping error: {e!s}")
async def _wait_for_token_creation(self, websocket) -> TokenInfo | None:
"""Wait for token creation events from any platform.
Args:
websocket: Active WebSocket connection
Returns:
TokenInfo if a token creation is found, None otherwise
"""
"""Wait for token creation events from any platform."""
try:
response = await asyncio.wait_for(websocket.recv(), timeout=30)
data = json.loads(response)
+15 -9
View File
@@ -32,7 +32,7 @@ class PlatformRegistry:
def __init__(self):
self._implementations: dict[Platform, dict[str, type]] = {}
self._instances: dict[Platform, PlatformImplementations] = {}
self._instances: dict[tuple[Platform, str], PlatformImplementations] = {}
def register_platform(
self,
@@ -80,13 +80,16 @@ class PlatformRegistry:
if platform not in self._implementations:
raise ValueError(f"Platform {platform} is not registered")
# Check if we already have instances for this platform
if platform in self._instances:
return self._instances[platform]
# Use client address as cache key to allow multiple clients
cache_key = (platform, str(client.rpc_endpoint))
# Check if we already have instances for this platform + client combo
if cache_key in self._instances:
return self._instances[cache_key]
impl_classes = self._implementations[platform]
# Create instances
# Create instances - only curve_manager needs client
address_provider = impl_classes['address_provider']()
instruction_builder = impl_classes['instruction_builder']()
curve_manager = impl_classes['curve_manager'](client)
@@ -100,20 +103,22 @@ class PlatformRegistry:
)
# Cache the instances
self._instances[platform] = implementations
self._instances[cache_key] = implementations
return implementations
def get_platform_implementations(self, platform: Platform) -> PlatformImplementations | None:
def get_platform_implementations(self, platform: Platform, client_endpoint: str) -> PlatformImplementations | None:
"""Get cached platform implementations.
Args:
platform: Platform to get implementations for
client_endpoint: Client endpoint for cache lookup
Returns:
PlatformImplementations if available, None otherwise
"""
return self._instances.get(platform)
cache_key = (platform, client_endpoint)
return self._instances.get(cache_key)
def get_supported_platforms(self) -> list[Platform]:
"""Get list of supported platforms.
@@ -144,7 +149,7 @@ class PlatformFactory:
def _setup_default_platforms(self) -> None:
"""Setup default platform registrations."""
# Import platform implementations dynamically to avoid circular imports
# Import and register pump.fun platform
try:
from platforms.pumpfun import (
PumpFunAddressProvider,
@@ -164,6 +169,7 @@ class PlatformFactory:
except ImportError as e:
print(f"Warning: Could not register pump.fun platform: {e}")
# Import and register LetsBonk platform
try:
from platforms.letsbonk import (
LetsBonkAddressProvider,
+105 -30
View File
@@ -5,11 +5,13 @@ This module parses LetsBonk-specific token creation events from various sources
by implementing the EventParser interface.
"""
import base64
import struct
from time import monotonic
from typing import Any, Final
from solders.pubkey import Pubkey
from solders.transaction import VersionedTransaction
from interfaces.core import EventParser, Platform, TokenInfo
from platforms.letsbonk.address_provider import LetsBonkAddressProvider
@@ -184,6 +186,108 @@ class LetsBonkEventParser(EventParser):
"""
return [self.INITIALIZE_DISCRIMINATOR]
def parse_token_creation_from_block(self, block_data: dict) -> TokenInfo | None:
"""Parse token creation from block data (for block listener).
Args:
block_data: Block data from WebSocket
Returns:
TokenInfo if token creation found, None otherwise
"""
try:
if "transactions" not in block_data:
return None
for tx in block_data["transactions"]:
if not isinstance(tx, dict) or "transaction" not in tx:
continue
# Decode base64 transaction data if needed
tx_data = tx["transaction"]
if isinstance(tx_data, list) and len(tx_data) > 0:
try:
tx_data_encoded = tx_data[0]
tx_data_decoded = base64.b64decode(tx_data_encoded)
transaction = VersionedTransaction.from_bytes(tx_data_decoded)
for ix in transaction.message.instructions:
program_id = transaction.message.account_keys[ix.program_id_index]
# Check if instruction is from LetsBonk program
if str(program_id) != str(self.get_program_id()):
continue
ix_data = bytes(ix.data)
# Check for initialize discriminator
if len(ix_data) >= 8:
discriminator = struct.unpack("<Q", ix_data[:8])[0]
if discriminator == self.INITIALIZE_DISCRIMINATOR_INT:
# Token creation should have substantial data and many accounts
if len(ix_data) <= 8 or len(ix.accounts) < 10:
continue
# Parse the instruction
token_info = self.parse_token_creation_from_instruction(
ix_data, ix.accounts, transaction.message.account_keys
)
if token_info:
return token_info
except Exception:
continue
# Handle already decoded transaction data
elif isinstance(tx_data, dict) and "message" in tx_data:
try:
message = tx_data["message"]
if "instructions" not in message or "accountKeys" not in message:
continue
for ix in message["instructions"]:
if "programIdIndex" not in ix or "accounts" not in ix or "data" not in ix:
continue
program_idx = ix["programIdIndex"]
if program_idx >= len(message["accountKeys"]):
continue
program_id_str = message["accountKeys"][program_idx]
if program_id_str != str(self.get_program_id()):
continue
# Decode instruction data
ix_data = base64.b64decode(ix["data"])
if len(ix_data) >= 8:
discriminator = struct.unpack("<Q", ix_data[:8])[0]
if discriminator == self.INITIALIZE_DISCRIMINATOR_INT:
if len(ix_data) <= 8 or len(ix["accounts"]) < 10:
continue
# Convert account keys to bytes for parsing
account_keys_bytes = [
Pubkey.from_string(key).to_bytes()
for key in message["accountKeys"]
]
token_info = self.parse_token_creation_from_instruction(
ix_data, ix["accounts"], account_keys_bytes
)
if token_info:
return token_info
except Exception:
continue
return None
except Exception:
return None
def _parse_initialize_instruction_data(self, data: bytes) -> dict | None:
"""Parse the initialize instruction data from LetsBonk.
@@ -240,33 +344,4 @@ class LetsBonkEventParser(EventParser):
return parsed_data
except Exception:
return None
def parse_token_creation_from_block(self, block_data: dict) -> list[TokenInfo]:
"""Parse token creations from block data (for block listener).
Args:
block_data: Block data from WebSocket
Returns:
List of TokenInfo for any token creations found
"""
tokens = []
try:
if "transactions" not in block_data:
return tokens
for tx in block_data["transactions"]:
if not isinstance(tx, dict) or "transaction" not in tx:
continue
# Process transaction (implementation would be similar to pump.fun)
# This is a simplified version - full implementation would decode
# the transaction and check for LetsBonk initialize instructions
pass
return tokens
except Exception:
return tokens
return None
+29 -74
View File
@@ -1,8 +1,6 @@
"""
Platform-aware trader implementations that use the interface system.
This module provides new trader classes that work with any platform
through the interface system, while maintaining compatibility with existing code.
Final cleanup removing all platform-specific hardcoding.
"""
from solders.pubkey import Pubkey
@@ -33,18 +31,7 @@ class PlatformAwareBuyer(Trader):
extreme_fast_token_amount: int = 0,
extreme_fast_mode: bool = False,
):
"""Initialize platform-aware token buyer.
Args:
client: Solana client for RPC calls
wallet: Wallet for signing transactions
priority_fee_manager: Priority fee manager
amount: Amount of SOL to spend
slippage: Slippage tolerance (0.01 = 1%)
max_retries: Maximum number of retry attempts
extreme_fast_token_amount: Amount of token to buy if extreme fast mode is enabled
extreme_fast_mode: If enabled, avoid fetching pool state for price estimation
"""
"""Initialize platform-aware token buyer."""
self.client = client
self.wallet = wallet
self.priority_fee_manager = priority_fee_manager
@@ -55,14 +42,7 @@ class PlatformAwareBuyer(Trader):
self.extreme_fast_token_amount = extreme_fast_token_amount
async def execute(self, token_info: TokenInfo, *args, **kwargs) -> TradeResult:
"""Execute buy operation using platform-specific implementations.
Args:
token_info: Enhanced token information with platform
Returns:
TradeResult with buy outcome
"""
"""Execute buy operation using platform-specific implementations."""
try:
# Get platform-specific implementations
implementations = get_platform_implementations(token_info.platform, self.client)
@@ -78,7 +58,7 @@ class PlatformAwareBuyer(Trader):
token_amount = self.extreme_fast_token_amount
token_price_sol = self.amount / token_amount if token_amount > 0 else 0
else:
# Get pool address based on platform
# Get pool address based on platform using platform-agnostic method
pool_address = self._get_pool_address(token_info, address_provider)
# Regular behavior with RPC call
@@ -92,7 +72,7 @@ class PlatformAwareBuyer(Trader):
# Calculate maximum SOL to spend with slippage
max_amount_lamports = int(amount_lamports * (1 + self.slippage))
# Build buy instructions
# Build buy instructions using platform-specific builder
instructions = await instruction_builder.build_buy_instruction(
token_info,
self.wallet.pubkey,
@@ -107,7 +87,7 @@ class PlatformAwareBuyer(Trader):
)
logger.info(
f"Buying {token_amount:.6f} tokens at {token_price_sol:.8f} SOL per token"
f"Buying {token_amount:.6f} tokens at {token_price_sol:.8f} SOL per token on {token_info.platform.value}"
)
logger.info(
f"Total cost: {self.amount:.6f} SOL (max: {max_amount_lamports / LAMPORTS_PER_SOL:.6f} SOL)"
@@ -151,22 +131,17 @@ class PlatformAwareBuyer(Trader):
)
def _get_pool_address(self, token_info: TokenInfo, address_provider: AddressProvider) -> Pubkey:
"""Get the pool/curve address for price calculations.
Args:
token_info: Token information
address_provider: Platform address provider
Returns:
Pool/curve address
"""
"""Get the pool/curve address for price calculations using platform-agnostic method."""
# Try to get the address from token_info first, then derive if needed
if token_info.platform == Platform.PUMP_FUN:
return token_info.bonding_curve or address_provider.derive_pool_address(token_info.mint)
if hasattr(token_info, 'bonding_curve') and token_info.bonding_curve:
return token_info.bonding_curve
elif token_info.platform == Platform.LETS_BONK:
return token_info.pool_state or address_provider.derive_pool_address(token_info.mint)
else:
# Fallback to deriving the address
return address_provider.derive_pool_address(token_info.mint)
if hasattr(token_info, 'pool_state') and token_info.pool_state:
return token_info.pool_state
# Fallback to deriving the address using platform provider
return address_provider.derive_pool_address(token_info.mint)
class PlatformAwareSeller(Trader):
@@ -180,15 +155,7 @@ class PlatformAwareSeller(Trader):
slippage: float = 0.25,
max_retries: int = 5,
):
"""Initialize platform-aware token seller.
Args:
client: Solana client for RPC calls
wallet: Wallet for signing transactions
priority_fee_manager: Priority fee manager
slippage: Slippage tolerance (0.25 = 25%)
max_retries: Maximum number of retry attempts
"""
"""Initialize platform-aware token seller."""
self.client = client
self.wallet = wallet
self.priority_fee_manager = priority_fee_manager
@@ -196,14 +163,7 @@ class PlatformAwareSeller(Trader):
self.max_retries = max_retries
async def execute(self, token_info: TokenInfo, *args, **kwargs) -> TradeResult:
"""Execute sell operation using platform-specific implementations.
Args:
token_info: Enhanced token information with platform
Returns:
TradeResult with sell outcome
"""
"""Execute sell operation using platform-specific implementations."""
try:
# Get platform-specific implementations
implementations = get_platform_implementations(token_info.platform, self.client)
@@ -229,7 +189,7 @@ class PlatformAwareSeller(Trader):
error_message="No tokens to sell"
)
# Get pool address and current price
# Get pool address and current price using platform-agnostic method
pool_address = self._get_pool_address(token_info, address_provider)
token_price_sol = await curve_manager.calculate_price(pool_address)
@@ -239,13 +199,13 @@ class PlatformAwareSeller(Trader):
expected_sol_output = float(token_balance_decimal) * float(token_price_sol)
min_sol_output = int((expected_sol_output * (1 - self.slippage)) * LAMPORTS_PER_SOL)
logger.info(f"Selling {token_balance_decimal} tokens")
logger.info(f"Selling {token_balance_decimal} tokens on {token_info.platform.value}")
logger.info(f"Expected SOL output: {expected_sol_output:.8f} SOL")
logger.info(
f"Minimum SOL output (with {self.slippage * 100}% slippage): {min_sol_output / LAMPORTS_PER_SOL:.8f} SOL"
)
# Build sell instructions
# Build sell instructions using platform-specific builder
instructions = await instruction_builder.build_sell_instruction(
token_info,
self.wallet.pubkey,
@@ -297,19 +257,14 @@ class PlatformAwareSeller(Trader):
)
def _get_pool_address(self, token_info: TokenInfo, address_provider: AddressProvider) -> Pubkey:
"""Get the pool/curve address for price calculations.
Args:
token_info: Token information
address_provider: Platform address provider
Returns:
Pool/curve address
"""
"""Get the pool/curve address for price calculations using platform-agnostic method."""
# Try to get the address from token_info first, then derive if needed
if token_info.platform == Platform.PUMP_FUN:
return token_info.bonding_curve or address_provider.derive_pool_address(token_info.mint)
if hasattr(token_info, 'bonding_curve') and token_info.bonding_curve:
return token_info.bonding_curve
elif token_info.platform == Platform.LETS_BONK:
return token_info.pool_state or address_provider.derive_pool_address(token_info.mint)
else:
# Fallback to deriving the address
return address_provider.derive_pool_address(token_info.mint)
if hasattr(token_info, 'pool_state') and token_info.pool_state:
return token_info.pool_state
# Fallback to deriving the address using platform provider
return address_provider.derive_pool_address(token_info.mint)
+46 -95
View File
@@ -1,6 +1,6 @@
"""
Universal trading coordinator that works with any platform.
Replaces PumpTrader with platform-agnostic implementation.
Cleaned up to remove all platform-specific hardcoding.
"""
import asyncio
@@ -84,18 +84,7 @@ class UniversalTrader:
marry_mode: bool = False,
yolo_mode: bool = False,
):
"""Initialize the universal trader.
Args:
rpc_endpoint: RPC endpoint URL
wss_endpoint: WebSocket endpoint URL
private_key: Wallet private key
buy_amount: Amount of SOL to spend on buys
buy_slippage: Slippage tolerance for buys
sell_slippage: Slippage tolerance for sells
platform: Platform to trade on (Platform enum or string)
... (other args same as PumpTrader)
"""
"""Initialize the universal trader."""
# Core components
self.solana_client = SolanaClient(rpc_endpoint)
self.wallet = Wallet(private_key)
@@ -116,6 +105,15 @@ class UniversalTrader:
logger.info(f"Initialized Universal Trader for platform: {self.platform.value}")
# Validate platform support
try:
from platforms import platform_factory
if not platform_factory.registry.is_platform_supported(self.platform):
raise ValueError(f"Platform {self.platform.value} is not supported")
except Exception as e:
logger.error(f"Platform validation failed: {e}")
raise
# Get platform-specific implementations
self.platform_implementations = get_platform_implementations(
self.platform, self.solana_client
@@ -141,7 +139,7 @@ class UniversalTrader:
max_retries,
)
# Initialize the appropriate listener
# Initialize the appropriate listener with platform filtering
self.token_listener = ListenerFactory.create_listener(
listener_type=listener_type,
wss_endpoint=wss_endpoint,
@@ -253,11 +251,7 @@ class UniversalTrader:
logger.info("Universal Trader has shut down")
async def _wait_for_token(self) -> TokenInfo | None:
"""Wait for a single token to be detected.
Returns:
TokenInfo or None if timeout occurs
"""
"""Wait for a single token to be detected."""
# Create a one-time event to signal when a token is found
token_found = asyncio.Event()
found_token = None
@@ -322,11 +316,7 @@ class UniversalTrader:
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
"""
"""Queue a token for processing if not already processed."""
token_key = str(token_info.mint)
if token_key in self.processed_tokens:
@@ -360,7 +350,6 @@ class UniversalTrader:
await self._handle_token(token_info)
except asyncio.CancelledError:
# Handle cancellation gracefully
logger.info("Token queue processor was cancelled")
break
except Exception as e:
@@ -369,11 +358,7 @@ class UniversalTrader:
self.token_queue.task_done()
async def _handle_token(self, token_info: TokenInfo) -> None:
"""Handle a new token creation event.
Args:
token_info: Token information
"""
"""Handle a new token creation event."""
try:
# Validate that token is for our platform
if token_info.platform != self.platform:
@@ -404,12 +389,7 @@ class UniversalTrader:
logger.error(f"Error handling token {token_info.symbol}: {e!s}")
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
"""
"""Handle successful token purchase."""
logger.info(f"Successfully bought {token_info.symbol} on {token_info.platform.value}")
self._log_trade("buy", token_info, buy_result.price, buy_result.amount, buy_result.tx_signature)
self.traded_mints.add(token_info.mint)
@@ -426,12 +406,7 @@ class UniversalTrader:
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
"""
"""Handle failed token purchase."""
logger.error(f"Failed to buy {token_info.symbol}: {buy_result.error_message}")
# Close ATA if enabled
await handle_cleanup_after_failure(
@@ -445,12 +420,7 @@ class UniversalTrader:
)
async def _handle_tp_sl_exit(self, token_info: TokenInfo, buy_result: TradeResult) -> None:
"""Handle take profit/stop loss exit strategy.
Args:
token_info: Token information
buy_result: Result from the buy operation
"""
"""Handle take profit/stop loss exit strategy."""
# Create position
position = Position.create_from_buy_result(
mint=token_info.mint,
@@ -472,11 +442,7 @@ class UniversalTrader:
await self._monitor_position_until_exit(token_info, position)
async def _handle_time_based_exit(self, token_info: TokenInfo) -> None:
"""Handle legacy time-based exit strategy.
Args:
token_info: Token information
"""
"""Handle legacy time-based exit strategy."""
logger.info(f"Waiting for {self.wait_time_after_buy} seconds before selling...")
await asyncio.sleep(self.wait_time_after_buy)
@@ -500,15 +466,10 @@ class UniversalTrader:
logger.error(f"Failed to sell {token_info.symbol}: {sell_result.error_message}")
async def _monitor_position_until_exit(self, token_info: TokenInfo, position: Position) -> None:
"""Monitor a position until exit conditions are met.
Args:
token_info: Token information
position: Position to monitor
"""
"""Monitor a position until exit conditions are met."""
logger.info(f"Starting position monitoring (check interval: {self.price_check_interval}s)")
# Get pool address for price monitoring
# Get pool address for price monitoring using platform-agnostic method
pool_address = self._get_pool_address(token_info)
curve_manager = self.platform_implementations.curve_manager
@@ -570,35 +531,25 @@ class UniversalTrader:
await asyncio.sleep(self.price_check_interval) # Continue monitoring despite errors
def _get_pool_address(self, token_info: TokenInfo) -> Pubkey:
"""Get the pool/curve address for price monitoring.
Args:
token_info: Token information
Returns:
Pool/curve address
"""
"""Get the pool/curve address for price monitoring using platform-agnostic method."""
address_provider = self.platform_implementations.address_provider
if token_info.platform == Platform.PUMP_FUN:
return token_info.bonding_curve or address_provider.derive_pool_address(token_info.mint)
elif token_info.platform == Platform.LETS_BONK:
return token_info.pool_state or address_provider.derive_pool_address(token_info.mint)
# Use platform-specific logic to get the appropriate address
if hasattr(token_info, 'bonding_curve') and token_info.bonding_curve:
return token_info.bonding_curve
elif hasattr(token_info, 'pool_state') and token_info.pool_state:
return token_info.pool_state
else:
# Fallback to deriving the address
# Fallback to deriving the address using platform provider
return address_provider.derive_pool_address(token_info.mint)
async def _save_token_info(self, token_info: TokenInfo) -> None:
"""Save token information to a file.
Args:
token_info: Token information
"""
"""Save token information to a file."""
try:
os.makedirs("trades", exist_ok=True)
file_name = os.path.join("trades", f"{token_info.mint}.txt")
# Convert to dictionary for saving
# Convert to dictionary for saving - platform-agnostic
token_dict = {
"name": token_info.name,
"symbol": token_info.symbol,
@@ -607,14 +558,22 @@ class UniversalTrader:
"platform": token_info.platform.value,
"user": str(token_info.user) if token_info.user else None,
"creator": str(token_info.creator) if token_info.creator else None,
# Platform-specific fields
"bonding_curve": str(token_info.bonding_curve) if token_info.bonding_curve else None,
"associated_bonding_curve": str(token_info.associated_bonding_curve) if token_info.associated_bonding_curve else None,
"creator_vault": str(token_info.creator_vault) if token_info.creator_vault else None,
"pool_state": str(token_info.pool_state) if token_info.pool_state else None,
"base_vault": str(token_info.base_vault) if token_info.base_vault else None,
"quote_vault": str(token_info.quote_vault) if token_info.quote_vault else None,
"creation_timestamp": token_info.creation_timestamp,
}
# Add platform-specific fields only if they exist
platform_fields = {
"bonding_curve": token_info.bonding_curve,
"associated_bonding_curve": token_info.associated_bonding_curve,
"creator_vault": token_info.creator_vault,
"pool_state": token_info.pool_state,
"base_vault": token_info.base_vault,
"quote_vault": token_info.quote_vault,
}
for field_name, field_value in platform_fields.items():
if field_value is not None:
token_dict[field_name] = str(field_value)
with open(file_name, "w") as file:
file.write(json.dumps(token_dict, indent=2))
@@ -624,15 +583,7 @@ class UniversalTrader:
logger.error(f"Failed to save token information: {e!s}")
def _log_trade(self, action: str, token_info: TokenInfo, price: float, amount: float, tx_hash: str | None) -> None:
"""Log trade information.
Args:
action: Trade action (buy/sell)
token_info: Token information
price: Token price in SOL
amount: Trade amount in SOL
tx_hash: Transaction hash
"""
"""Log trade information."""
try:
os.makedirs("trades", exist_ok=True)