Reduce RPC delays when selling (#150)

* feat(trading): enhance sell execution with token amount and price parameters to reduce RPC delays

* feat(trading): add method to fetch actual token balance after transaction to improve accuracy

* feat(trading): update account data size limit and enhance price calculations
This commit is contained in:
Anton Sauchyk
2025-11-23 12:02:29 +01:00
committed by GitHub
parent 03a4e7bcbc
commit 9d93d859a8
7 changed files with 293 additions and 60 deletions
+107 -39
View File
@@ -130,6 +130,35 @@ class PlatformAwareBuyer(Trader):
if success:
logger.info(f"Buy transaction confirmed: {tx_signature}")
# Fetch actual tokens and SOL spent from transaction
# Uses preBalances/postBalances to get exact amounts
sol_destination = self._get_sol_destination(
token_info, address_provider
)
tokens_raw, sol_spent = await self.client.get_buy_transaction_details(
str(tx_signature), token_info.mint, sol_destination
)
if tokens_raw is not None and sol_spent is not None:
actual_amount = tokens_raw / 10**TOKEN_DECIMALS
actual_price = (sol_spent / LAMPORTS_PER_SOL) / actual_amount
logger.info(
f"Actual tokens received: {actual_amount:.6f} "
f"(expected: {token_amount:.6f})"
)
logger.info(
f"Actual SOL spent: {sol_spent / LAMPORTS_PER_SOL:.10f} SOL"
)
logger.info(f"Actual price: {actual_price:.10f} SOL/token")
token_amount = actual_amount
token_price_sol = actual_price
else:
raise ValueError(
f"Failed to parse transaction details: tokens={tokens_raw}, "
f"sol_spent={sol_spent}"
)
return TradeResult(
success=True,
platform=token_info.platform,
@@ -165,6 +194,42 @@ class PlatformAwareBuyer(Trader):
# Fallback to deriving the address using platform provider
return address_provider.derive_pool_address(token_info.mint)
def _get_sol_destination(
self, token_info: TokenInfo, address_provider: AddressProvider
) -> Pubkey:
"""Get the address where SOL is sent during a buy transaction.
For pump.fun: SOL goes to the bonding curve
For letsbonk: SOL goes to the quote_vault (WSOL vault)
Args:
token_info: Token information
address_provider: Platform-specific address provider
Returns:
Address where SOL is transferred during buy
Raises:
NotImplementedError: If platform SOL destination is not implemented
"""
if token_info.platform == Platform.PUMP_FUN:
# For pump.fun, SOL goes directly to bonding curve
if hasattr(token_info, "bonding_curve") and token_info.bonding_curve:
return token_info.bonding_curve
return address_provider.derive_pool_address(token_info.mint)
elif token_info.platform == Platform.LETS_BONK:
# For letsbonk, SOL goes to quote_vault (WSOL vault)
if hasattr(token_info, "quote_vault") and token_info.quote_vault:
return token_info.quote_vault
# Derive quote_vault if not available
return address_provider.derive_quote_vault(token_info.mint)
raise NotImplementedError(
f"SOL destination not implemented for platform {token_info.platform.value}. "
f"Add platform-specific logic to _get_sol_destination() to specify where "
f"SOL is transferred during buy transactions for this platform."
)
def _get_cu_override(self, operation: str, platform: Platform) -> int | None:
"""Get compute unit override from configuration.
@@ -202,8 +267,35 @@ class PlatformAwareSeller(Trader):
self.max_retries = max_retries
self.compute_units = compute_units or {}
async def execute(self, token_info: TokenInfo) -> TradeResult:
"""Execute sell operation using platform-specific implementations."""
async def execute(
self, token_info: TokenInfo, token_amount: float, token_price: float
) -> TradeResult:
"""Execute sell operation using platform-specific implementations.
Args:
token_info: Token information for the sell operation
token_amount: Token amount to sell (from buy result). Required to avoid
RPC balance query delays.
token_price: Token price in SOL (from buy result). Required to avoid
RPC pool state query delays.
Returns:
TradeResult with operation outcome
Raises:
ValueError: If required parameters are not provided
"""
if token_amount is None:
raise ValueError(
"token_amount is required for sell operation. "
"Pass the amount from buy result to avoid RPC delays."
)
if token_price is None or token_price <= 0:
raise ValueError(
"token_price is required for sell operation and must be positive. "
"Pass the price from buy result to avoid RPC delays."
)
try:
# Get platform-specific implementations
implementations = get_platform_implementations(
@@ -211,19 +303,14 @@ class PlatformAwareSeller(Trader):
)
address_provider = implementations.address_provider
instruction_builder = implementations.instruction_builder
curve_manager = implementations.curve_manager
# Get user's token account and balance
user_token_account = address_provider.derive_user_token_account(
self.wallet.pubkey, token_info.mint
)
# Use pre-known amount and price (no RPC delay)
token_balance_decimal = token_amount
token_balance = int(token_amount * 10**TOKEN_DECIMALS)
token_price_sol = token_price
token_balance = await self.client.get_token_account_balance(
user_token_account
)
token_balance_decimal = token_balance / 10**TOKEN_DECIMALS
logger.info(f"Token balance: {token_balance_decimal}")
logger.info(f"Token balance: {token_balance_decimal:.6f}")
logger.info(f"Price per Token (from buy): {token_price_sol:.8f} SOL")
if token_balance == 0:
logger.info("No tokens to sell.")
@@ -233,38 +320,19 @@ class PlatformAwareSeller(Trader):
error_message="No tokens to sell",
)
# Get pool address and current price using platform-agnostic method
pool_address = self._get_pool_address(token_info, address_provider)
# Fetch pool state to get price and mayhem mode status
pool_state = await curve_manager.get_pool_state(pool_address)
token_price_sol = pool_state.get("price_per_token")
# Validate price_per_token is present and positive
if token_price_sol is None or token_price_sol <= 0:
raise ValueError(
f"Invalid price_per_token: {token_price_sol} for pool {pool_address} "
f"(mint: {token_info.mint}) - cannot execute sell with zero/invalid price"
)
# Set is_mayhem_mode from bonding curve state
token_info.is_mayhem_mode = pool_state.get("is_mayhem_mode", False)
logger.info(f"Price per Token: {token_price_sol:.8f} SOL")
# Calculate expected SOL output
# Calculate expected SOL output with slippage protection
expected_sol_output = token_balance_decimal * token_price_sol
# Calculate minimum SOL output with slippage protection
min_sol_output = int(
(expected_sol_output * (1 - self.slippage)) * LAMPORTS_PER_SOL
min_sol_output = max(
1,
int((expected_sol_output * (1 - self.slippage)) * LAMPORTS_PER_SOL),
)
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"Expected SOL output: {expected_sol_output:.10f} SOL")
logger.info(
f"Minimum SOL output (with {self.slippage * 100:.1f}% slippage): {min_sol_output / LAMPORTS_PER_SOL:.8f} SOL"
f"Minimum SOL output (with {self.slippage * 100:.1f}% slippage): "
f"{min_sol_output / LAMPORTS_PER_SOL:.10f} SOL ({min_sol_output} lamports)"
)
# Build sell instructions using platform-specific builder
+24 -9
View File
@@ -193,7 +193,9 @@ class UniversalTrader:
# State tracking
self.traded_mints: set[Pubkey] = set()
self.traded_token_programs: dict[str, Pubkey] = {} # Maps mint (as string) to token_program_id
self.traded_token_programs: dict[
str, Pubkey
] = {} # Maps mint (as string) to token_program_id
self.token_queue: asyncio.Queue = asyncio.Queue()
self.processing: bool = False
self.processed_tokens: set[str] = set()
@@ -329,8 +331,7 @@ class UniversalTrader:
# Build parallel lists of mints and token_program_ids
mints_list = list(self.traded_mints)
token_program_ids = [
self.traded_token_programs.get(str(mint))
for mint in mints_list
self.traded_token_programs.get(str(mint)) for mint in mints_list
]
await handle_cleanup_post_session(
self.solana_client,
@@ -465,7 +466,7 @@ class UniversalTrader:
if self.exit_strategy == "tp_sl":
await self._handle_tp_sl_exit(token_info, buy_result)
elif self.exit_strategy == "time_based":
await self._handle_time_based_exit(token_info)
await self._handle_time_based_exit(token_info, buy_result)
elif self.exit_strategy == "manual":
logger.info("Manual exit strategy - position will remain open")
else:
@@ -512,13 +513,23 @@ class UniversalTrader:
# Monitor position until exit condition is met
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."""
async def _handle_time_based_exit(
self, token_info: TokenInfo, buy_result: TradeResult
) -> None:
"""Handle legacy time-based exit strategy.
Args:
token_info: Token information
buy_result: Result from the buy operation (contains token amount)
"""
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)
# Pass token amount and price from buy result to avoid RPC delays
sell_result: TradeResult = await self.seller.execute(
token_info, token_amount=buy_result.amount, token_price=buy_result.price
)
if sell_result.success:
logger.info(f"Successfully sold {token_info.symbol}")
@@ -575,8 +586,12 @@ class UniversalTrader:
f"Position PnL: {pnl['price_change_pct']:.2f}% ({pnl['unrealized_pnl_sol']:.6f} SOL)"
)
# Execute sell
sell_result = await self.seller.execute(token_info)
# Execute sell with position quantity and entry price to avoid RPC delays
sell_result = await self.seller.execute(
token_info,
token_amount=position.quantity,
token_price=position.entry_price,
)
if sell_result.success:
# Close position with actual exit price