feat: add simple tp and sl with price polling

This commit is contained in:
smypmsa
2025-06-15 18:20:13 +00:00
parent 185811d854
commit 473b1bec70
9 changed files with 500 additions and 38 deletions
+8 -1
View File
@@ -8,7 +8,7 @@ rpc_endpoint: "${SOLANA_NODE_RPC_ENDPOINT}"
wss_endpoint: "${SOLANA_NODE_WSS_ENDPOINT}" wss_endpoint: "${SOLANA_NODE_WSS_ENDPOINT}"
private_key: "${SOLANA_PRIVATE_KEY}" private_key: "${SOLANA_PRIVATE_KEY}"
enabled: false # You can turn off the bot w/o removing its config enabled: true # You can turn off the bot w/o removing its config
separate_process: true separate_process: true
# Geyser configuration (fastest method for getting updates) # Geyser configuration (fastest method for getting updates)
@@ -24,6 +24,13 @@ trade:
buy_slippage: 0.3 # Maximum acceptable price deviation (0.3 = 30%) buy_slippage: 0.3 # Maximum acceptable price deviation (0.3 = 30%)
sell_slippage: 0.3 sell_slippage: 0.3
# Exit strategy configuration
exit_strategy: "time_based" # Options: "time_based", "tp_sl", "manual"
#take_profit_percentage: 0.1 # Take profit at 10% gain (0.1 = 10%)
#stop_loss_percentage: 0.1 # Stop loss at 10% loss (0.1 = 10%)
max_hold_time: 15 # Maximum hold time in seconds
#price_check_interval: 2 # Check price every 2 seconds
# EXTREME FAST mode configuration # EXTREME FAST mode configuration
# When enabled, skips waiting for the bonding curve to stabilize and RPC price check. # 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. # The bot buys the specified number of tokens directly, making the process faster but less precise.
+7
View File
@@ -24,6 +24,13 @@ trade:
buy_slippage: 0.3 # Maximum acceptable price deviation (0.3 = 30%) buy_slippage: 0.3 # Maximum acceptable price deviation (0.3 = 30%)
sell_slippage: 0.3 sell_slippage: 0.3
# Exit strategy configuration
exit_strategy: "time_based" # Options: "time_based", "tp_sl", "manual"
#take_profit_percentage: 0.1 # Take profit at 10% gain (0.1 = 10%)
#stop_loss_percentage: 0.1 # Stop loss at 10% loss (0.1 = 10%)
max_hold_time: 15 # Maximum hold time in seconds
#price_check_interval: 2 # Check price every 2 seconds
# EXTREME FAST mode configuration # EXTREME FAST mode configuration
# When enabled, skips waiting for the bonding curve to stabilize and RPC price check. # 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. # The bot buys the specified number of tokens directly, making the process faster but less precise.
+7
View File
@@ -24,6 +24,13 @@ trade:
buy_slippage: 0.3 # Maximum acceptable price deviation (0.3 = 30%) buy_slippage: 0.3 # Maximum acceptable price deviation (0.3 = 30%)
sell_slippage: 0.3 sell_slippage: 0.3
# Exit strategy configuration
exit_strategy: "time_based" # Options: "time_based", "tp_sl", "manual"
#take_profit_percentage: 0.1 # Take profit at 10% gain (0.1 = 10%)
#stop_loss_percentage: 0.1 # Stop loss at 10% loss (0.1 = 10%)
max_hold_time: 15 # Maximum hold time in seconds
#price_check_interval: 2 # Check price every 2 seconds
# EXTREME FAST mode configuration # EXTREME FAST mode configuration
# When enabled, skips waiting for the bonding curve to stabilize and RPC price check. # 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. # The bot buys the specified number of tokens directly, making the process faster but less precise.
+8 -1
View File
@@ -8,7 +8,7 @@ rpc_endpoint: "${SOLANA_NODE_RPC_ENDPOINT}"
wss_endpoint: "${SOLANA_NODE_WSS_ENDPOINT}" wss_endpoint: "${SOLANA_NODE_WSS_ENDPOINT}"
private_key: "${SOLANA_PRIVATE_KEY}" private_key: "${SOLANA_PRIVATE_KEY}"
enabled: true # You can turn off the bot w/o removing its config enabled: false # You can turn off the bot w/o removing its config
separate_process: true separate_process: true
# PumpPortal configuration (optional - uses default URL if not specified) # PumpPortal configuration (optional - uses default URL if not specified)
@@ -22,6 +22,13 @@ trade:
buy_slippage: 0.3 # Maximum acceptable price deviation (0.3 = 30%) buy_slippage: 0.3 # Maximum acceptable price deviation (0.3 = 30%)
sell_slippage: 0.3 sell_slippage: 0.3
# Exit strategy configuration
exit_strategy: "tp_sl" # Options: "time_based", "tp_sl", "manual"
take_profit_percentage: 0.1 # Take profit at 10% gain (0.1 = 10%)
stop_loss_percentage: 0.1 # Stop loss at 10% loss (0.1 = 10%)
max_hold_time: 600 # Maximum hold time in seconds (600 = 10 minutes)
price_check_interval: 2 # Check price every 2 seconds
# EXTREME FAST mode configuration # EXTREME FAST mode configuration
# When enabled, skips waiting for the bonding curve to stabilize and RPC price check. # 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. # The bot buys the specified number of tokens directly, making the process faster but less precise.
+7
View File
@@ -54,6 +54,13 @@ async def start_bot(config_path: str):
extreme_fast_mode=cfg["trade"].get("extreme_fast_mode", False), extreme_fast_mode=cfg["trade"].get("extreme_fast_mode", False),
extreme_fast_token_amount=cfg["trade"].get("extreme_fast_token_amount", 30), 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 configuration
listener_type=cfg["filters"]["listener_type"], listener_type=cfg["filters"]["listener_type"],
+3 -2
View File
@@ -24,8 +24,9 @@ CONFIG_VALIDATION_RULES = [
# Valid values for enum-like fields # Valid values for enum-like fields
VALID_VALUES = { VALID_VALUES = {
"filters.listener_type": ["logs", "blocks", "geyser"], "filters.listener_type": ["logs", "blocks", "geyser", "pumpportal"],
"cleanup.mode": ["disabled", "on_fail", "after_sell", "post_session"] "cleanup.mode": ["disabled", "on_fail", "after_sell", "post_session"],
"trade.exit_strategy": ["time_based", "tp_sl", "manual"]
} }
+130 -2
View File
@@ -113,12 +113,17 @@ class TokenBuyer(Trader):
success = await self.client.confirm_transaction(tx_signature) success = await self.client.confirm_transaction(tx_signature)
if success: if success:
# Get actual execution data from bonding curve balance changes
actual_price, actual_tokens = await self._get_actual_execution_price(tx_signature, token_info)
logger.info(f"Buy transaction confirmed: {tx_signature}") logger.info(f"Buy transaction confirmed: {tx_signature}")
logger.info(f"Actual price paid to bonding curve: {actual_price:.8f} SOL per token")
return TradeResult( return TradeResult(
success=True, success=True,
tx_signature=tx_signature, tx_signature=tx_signature,
amount=token_amount, amount=actual_tokens, # Actual tokens received
price=token_price_sol, price=actual_price, # Actual price based on bonding curve SOL flow
) )
else: else:
return TradeResult( return TradeResult(
@@ -216,3 +221,126 @@ class TokenBuyer(Trader):
except Exception as e: except Exception as e:
logger.error(f"Buy transaction failed: {e!s}") logger.error(f"Buy transaction failed: {e!s}")
raise raise
async def _get_actual_execution_price(self, tx_signature: str, token_info: TokenInfo) -> tuple[float, float]:
"""Get actual execution price from bonding curve SOL balance changes."""
try:
client = await self.client.get_client()
tx_response = await client.get_transaction(
tx_signature,
encoding="jsonParsed",
commitment="confirmed",
max_supported_transaction_version=0
)
if not tx_response.value or not tx_response.value.transaction:
raise ValueError("Transaction not found")
meta = tx_response.value.transaction.meta
if not meta or not meta.pre_balances or not meta.post_balances:
raise ValueError("Transaction balance data not found")
# Get accounts - they're ParsedAccountTxStatus objects, need to extract pubkey
accounts = tx_response.value.transaction.transaction.message.account_keys
# Find bonding curve account index in the transaction
bonding_curve_index = None
for i, account in enumerate(accounts):
# Extract pubkey from ParsedAccountTxStatus object
account_pubkey = str(account.pubkey) if hasattr(account, 'pubkey') else str(account)
if account_pubkey == str(token_info.bonding_curve):
bonding_curve_index = i
break
if bonding_curve_index is None:
raise ValueError("Bonding curve not found in transaction accounts")
pre_balance_lamports = meta.pre_balances[bonding_curve_index]
post_balance_lamports = meta.post_balances[bonding_curve_index]
sol_sent_to_curve = (post_balance_lamports - pre_balance_lamports) / LAMPORTS_PER_SOL
if sol_sent_to_curve <= 0:
raise ValueError(f"No SOL sent to bonding curve: {sol_sent_to_curve}")
tokens_received = await self._get_tokens_received_from_tx(tx_response, token_info)
actual_price = sol_sent_to_curve / tokens_received
logger.info(f"Bonding curve received: {sol_sent_to_curve:.6f} SOL")
logger.info(f"We received: {tokens_received:.6f} tokens")
logger.info(f"Actual execution price: {actual_price:.8f} SOL per token")
return actual_price, tokens_received
except Exception as e:
logger.warning(f"Failed to get actual execution price from bonding curve: {e}")
# Fallback to EXTREME_FAST estimate
tokens_received = self.extreme_fast_token_amount if self.extreme_fast_mode else self.amount / await self.curve_manager.calculate_price(token_info.bonding_curve)
return self.amount / tokens_received, tokens_received
async def _get_tokens_received_from_tx(self, tx_response, token_info: TokenInfo) -> float:
"""Extract tokens received from transaction token balance changes."""
meta = tx_response.value.transaction.meta
pre_token_balance = 0
post_token_balance = 0
wallet_str = str(self.wallet.pubkey)
mint_str = str(token_info.mint)
if meta.pre_token_balances:
for balance in meta.pre_token_balances:
# Convert to string for comparison
balance_owner = str(balance.owner) if hasattr(balance, 'owner') else str(getattr(balance, 'owner', ''))
balance_mint = str(balance.mint) if hasattr(balance, 'mint') else str(getattr(balance, 'mint', ''))
if balance_owner == wallet_str and balance_mint == mint_str:
try:
# Try multiple ways to get the amount
if hasattr(balance, 'ui_token_amount'):
amount_obj = balance.ui_token_amount
if hasattr(amount_obj, 'amount') and amount_obj.amount is not None:
pre_token_balance = int(amount_obj.amount)
elif hasattr(amount_obj, 'ui_amount') and amount_obj.ui_amount is not None:
pre_token_balance = int(float(amount_obj.ui_amount) * (10**TOKEN_DECIMALS))
except (ValueError, TypeError) as e:
logger.warning(f"Error parsing pre-token balance: {e}")
break
# Check post-token balances
if meta.post_token_balances:
for balance in meta.post_token_balances:
# Convert to string for comparison
balance_owner = str(balance.owner) if hasattr(balance, 'owner') else str(getattr(balance, 'owner', ''))
balance_mint = str(balance.mint) if hasattr(balance, 'mint') else str(getattr(balance, 'mint', ''))
if balance_owner == wallet_str and balance_mint == mint_str:
try:
# Try multiple ways to get the amount
if hasattr(balance, 'ui_token_amount'):
amount_obj = balance.ui_token_amount
if hasattr(amount_obj, 'amount') and amount_obj.amount is not None:
post_token_balance = int(amount_obj.amount)
elif hasattr(amount_obj, 'ui_amount') and amount_obj.ui_amount is not None:
post_token_balance = int(float(amount_obj.ui_amount) * (10**TOKEN_DECIMALS))
except (ValueError, TypeError) as e:
logger.warning(f"Error parsing post-token balance: {e}")
break
# Calculate tokens received
if pre_token_balance == 0 and post_token_balance > 0:
tokens_received_raw = post_token_balance
else:
tokens_received_raw = post_token_balance - pre_token_balance
if tokens_received_raw <= 0:
logger.warning("Token balance search failed. Using fallback from EXTREME_FAST estimate.")
# Fallback: use the amount we know we bought
return self.extreme_fast_token_amount if self.extreme_fast_mode else 20.0
return tokens_received_raw / 10**TOKEN_DECIMALS
+160
View File
@@ -0,0 +1,160 @@
"""
Position management for take profit/stop loss functionality.
"""
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from solders.pubkey import Pubkey
class ExitReason(Enum):
"""Reasons for position exit."""
TAKE_PROFIT = "take_profit"
STOP_LOSS = "stop_loss"
MAX_HOLD_TIME = "max_hold_time"
MANUAL = "manual"
@dataclass
class Position:
"""Represents an active trading position."""
# Token information
mint: Pubkey
symbol: str
# Position details
entry_price: float
quantity: float
entry_time: datetime
# Exit conditions
take_profit_price: float | None = None
stop_loss_price: float | None = None
max_hold_time: int | None = None # seconds
# Status
is_active: bool = True
exit_reason: ExitReason | None = None
exit_price: float | None = None
exit_time: datetime | None = None
@classmethod
def create_from_buy_result(
cls,
mint: Pubkey,
symbol: str,
entry_price: float,
quantity: float,
take_profit_percentage: float | None = None,
stop_loss_percentage: float | None = None,
max_hold_time: int | None = None,
) -> "Position":
"""Create a position from a successful buy transaction.
Args:
mint: Token mint address
symbol: Token symbol
entry_price: Price at which position was entered
quantity: Quantity of tokens purchased
take_profit_percentage: Take profit percentage (0.5 = 50% profit)
stop_loss_percentage: Stop loss percentage (0.2 = 20% loss)
max_hold_time: Maximum hold time in seconds
Returns:
Position instance
"""
take_profit_price = None
if take_profit_percentage is not None:
take_profit_price = entry_price * (1 + take_profit_percentage)
stop_loss_price = None
if stop_loss_percentage is not None:
stop_loss_price = entry_price * (1 - stop_loss_percentage)
return cls(
mint=mint,
symbol=symbol,
entry_price=entry_price,
quantity=quantity,
entry_time=datetime.utcnow(),
take_profit_price=take_profit_price,
stop_loss_price=stop_loss_price,
max_hold_time=max_hold_time,
)
def should_exit(self, current_price: float) -> tuple[bool, ExitReason | None]:
"""Check if position should be exited based on current conditions.
Args:
current_price: Current token price
Returns:
Tuple of (should_exit, exit_reason)
"""
if not self.is_active:
return False, None
# Check take profit
if self.take_profit_price and current_price >= self.take_profit_price:
return True, ExitReason.TAKE_PROFIT
# Check stop loss
if self.stop_loss_price and current_price <= self.stop_loss_price:
return True, ExitReason.STOP_LOSS
# Check max hold time
if self.max_hold_time:
elapsed_time = (datetime.utcnow() - self.entry_time).total_seconds()
if elapsed_time >= self.max_hold_time:
return True, ExitReason.MAX_HOLD_TIME
return False, None
def close_position(self, exit_price: float, exit_reason: ExitReason) -> None:
"""Close the position with exit details.
Args:
exit_price: Price at which position was exited
exit_reason: Reason for exit
"""
self.is_active = False
self.exit_price = exit_price
self.exit_reason = exit_reason
self.exit_time = datetime.utcnow()
def get_pnl(self, current_price: float | None = None) -> dict:
"""Calculate profit/loss for the position.
Args:
current_price: Current price (uses exit_price if position is closed)
Returns:
Dictionary with PnL information
"""
if self.is_active and current_price is None:
raise ValueError("current_price required for active position")
price_to_use = self.exit_price if not self.is_active else current_price
if price_to_use is None:
raise ValueError("No price available for PnL calculation")
price_change = price_to_use - self.entry_price
price_change_pct = (price_change / self.entry_price) * 100
unrealized_pnl = price_change * self.quantity
return {
"entry_price": self.entry_price,
"current_price": price_to_use,
"price_change": price_change,
"price_change_pct": price_change_pct,
"unrealized_pnl_sol": unrealized_pnl,
"quantity": self.quantity,
}
def __str__(self) -> str:
"""String representation of position."""
status = "ACTIVE" if self.is_active else f"CLOSED ({self.exit_reason.value})"
return f"Position({self.symbol}: {self.quantity:.6f} @ {self.entry_price:.8f} SOL - {status})"
+170 -32
View File
@@ -28,6 +28,7 @@ from monitoring.logs_listener import LogsListener
from monitoring.pumpportal_listener import PumpPortalListener from monitoring.pumpportal_listener import PumpPortalListener
from trading.base import TokenInfo, TradeResult from trading.base import TokenInfo, TradeResult
from trading.buyer import TokenBuyer from trading.buyer import TokenBuyer
from trading.position import Position
from trading.seller import TokenSeller from trading.seller import TokenSeller
from utils.logger import get_logger from utils.logger import get_logger
@@ -55,6 +56,13 @@ class PumpTrader:
extreme_fast_mode: bool = False, extreme_fast_mode: bool = False,
extreme_fast_token_amount: int = 30, extreme_fast_token_amount: int = 30,
# Exit strategy configuration
exit_strategy: str = "time_based",
take_profit_percentage: float | None = None,
stop_loss_percentage: float | None = None,
max_hold_time: int | None = None,
price_check_interval: int = 10,
# Priority fee configuration # Priority fee configuration
enable_dynamic_priority_fee: bool = False, enable_dynamic_priority_fee: bool = False,
enable_fixed_priority_fee: bool = True, enable_fixed_priority_fee: bool = True,
@@ -99,6 +107,12 @@ class PumpTrader:
extreme_fast_mode: Whether to enable extreme fast mode extreme_fast_mode: Whether to enable extreme fast mode
extreme_fast_token_amount: Maximum token amount for extreme fast mode extreme_fast_token_amount: Maximum token amount for extreme fast mode
exit_strategy: Exit strategy ("time_based", "tp_sl", or "manual")
take_profit_percentage: Take profit percentage (0.5 = 50% profit)
stop_loss_percentage: Stop loss percentage (0.2 = 20% loss)
max_hold_time: Maximum hold time in seconds
price_check_interval: How often to check price for TP/SL (seconds)
enable_dynamic_priority_fee: Whether to enable dynamic priority fees enable_dynamic_priority_fee: Whether to enable dynamic priority fees
enable_fixed_priority_fee: Whether to enable fixed priority fees enable_fixed_priority_fee: Whether to enable fixed priority fees
fixed_priority_fee: Fixed priority fee amount fixed_priority_fee: Fixed priority fee amount
@@ -183,6 +197,13 @@ class PumpTrader:
self.extreme_fast_mode = extreme_fast_mode self.extreme_fast_mode = extreme_fast_mode
self.extreme_fast_token_amount = extreme_fast_token_amount self.extreme_fast_token_amount = extreme_fast_token_amount
# Exit strategy parameters
self.exit_strategy = exit_strategy.lower()
self.take_profit_percentage = take_profit_percentage
self.stop_loss_percentage = stop_loss_percentage
self.max_hold_time = max_hold_time
self.price_check_interval = price_check_interval
# Timing parameters # Timing parameters
self.wait_time_after_creation = wait_time_after_creation self.wait_time_after_creation = wait_time_after_creation
self.wait_time_after_buy = wait_time_after_buy self.wait_time_after_buy = wait_time_after_buy
@@ -215,6 +236,11 @@ class PumpTrader:
logger.info(f"Creator filter: {self.bro_address if self.bro_address 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"Marry mode: {self.marry_mode}")
logger.info(f"YOLO mode: {self.yolo_mode}") logger.info(f"YOLO mode: {self.yolo_mode}")
logger.info(f"Exit strategy: {self.exit_strategy}")
if self.exit_strategy == "tp_sl":
logger.info(f"Take profit: {self.take_profit_percentage * 100 if self.take_profit_percentage else 'None'}%")
logger.info(f"Stop loss: {self.stop_loss_percentage * 100 if self.stop_loss_percentage else 'None'}%")
logger.info(f"Max hold time: {self.max_hold_time if self.max_hold_time else 'None'} seconds")
logger.info(f"Max token age: {self.max_token_age} seconds") logger.info(f"Max token age: {self.max_token_age} seconds")
try: try:
@@ -445,39 +471,14 @@ class PumpTrader:
) )
self.traded_mints.add(token_info.mint) self.traded_mints.add(token_info.mint)
# Sell token if not in marry mode # Choose exit strategy
if not self.marry_mode: if not self.marry_mode:
logger.info( if self.exit_strategy == "tp_sl":
f"Waiting for {self.wait_time_after_buy} seconds before selling..." await self._handle_tp_sl_exit(token_info, buy_result)
) elif self.exit_strategy == "time_based":
await asyncio.sleep(self.wait_time_after_buy) await self._handle_time_based_exit(token_info)
elif self.exit_strategy == "manual":
logger.info(f"Selling {token_info.symbol}...") logger.info("Manual exit strategy - position will remain open")
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: else:
logger.info("Marry mode enabled. Skipping sell operation.") logger.info("Marry mode enabled. Skipping sell operation.")
@@ -504,6 +505,143 @@ class PumpTrader:
self.cleanup_force_close_with_burn self.cleanup_force_close_with_burn
) )
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
"""
# Create position
position = Position.create_from_buy_result(
mint=token_info.mint,
symbol=token_info.symbol,
entry_price=buy_result.price, # type: ignore
quantity=buy_result.amount, # type: ignore
take_profit_percentage=self.take_profit_percentage,
stop_loss_percentage=self.stop_loss_percentage,
max_hold_time=self.max_hold_time,
)
logger.info(f"Created position: {position}")
if position.take_profit_price:
logger.info(f"Take profit target: {position.take_profit_price:.8f} SOL")
if position.stop_loss_price:
logger.info(f"Stop loss target: {position.stop_loss_price:.8f} SOL")
# 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.
Args:
token_info: Token information
"""
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}"
)
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
"""
logger.info(f"Starting position monitoring (check interval: {self.price_check_interval}s)")
while position.is_active:
try:
# Get current price from bonding curve
current_price = await self.curve_manager.calculate_price(token_info.bonding_curve)
# Check if position should be exited
should_exit, exit_reason = position.should_exit(current_price)
if should_exit and exit_reason:
logger.info(f"Exit condition met: {exit_reason.value}")
logger.info(f"Current price: {current_price:.8f} SOL")
# Log PnL before exit
pnl = position.get_pnl(current_price)
logger.info(f"Position PnL: {pnl['price_change_pct']:.2f}% ({pnl['unrealized_pnl_sol']:.6f} SOL)")
# Execute sell
sell_result = await self.seller.execute(token_info)
if sell_result.success:
# Close position with actual exit price
position.close_position(sell_result.price, exit_reason) # type: ignore
logger.info(f"Successfully exited position: {exit_reason.value}")
self._log_trade(
"sell",
token_info,
sell_result.price, # type: ignore
sell_result.amount, # type: ignore
sell_result.tx_signature,
)
# Log final PnL
final_pnl = position.get_pnl()
logger.info(f"Final PnL: {final_pnl['price_change_pct']:.2f}% ({final_pnl['unrealized_pnl_sol']:.6f} SOL)")
# 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 exit position: {sell_result.error_message}")
# Keep monitoring in case sell can be retried
break
else:
# Log current status
pnl = position.get_pnl(current_price)
logger.debug(f"Position status: {current_price:.8f} SOL ({pnl['price_change_pct']:+.2f}%)")
# Wait before next price check
await asyncio.sleep(self.price_check_interval)
except Exception as e:
logger.error(f"Error monitoring position: {e}")
await asyncio.sleep(self.price_check_interval) # Continue monitoring despite errors
async def _save_token_info( async def _save_token_info(
self, token_info: TokenInfo self, token_info: TokenInfo
) -> None: ) -> None: